逐步解說:使用 Visual Basic 撰寫簡單的多執行緒元件
BackgroundWorker 元件會取代並加入功能至 System.Threading 命名空間;不過,您可以依選擇為回溯相容性 (Backward Compatibility) 和未來使用而保留 System.Threading 命名空間。如需詳細資訊,請參閱 BackgroundWorker 元件概觀。
您可寫入能夠同時執行多個工作的應用程式。這種能力稱為「多執行緒處理」或「無限制執行緒」,對於設計耗用大量處理器資源並需使用者輸入的元件是一項強大的工具。計算薪資資訊的元件就是利用多執行緒處理的元件範例。這類元件可在一執行緒上處理由使用者輸入資料庫的資料,而同時又在另一個執行緒上執行需要大量處理器資源的薪資計算。由於這些處理程序是在不同的執行緒上執行,因此使用者不需要等到電腦完成計算後才輸入其他資料。在這份逐步解說中,您將建立可同時執行多項複雜計算的簡單多執行緒元件。
建立專案
您的應用程式將包括一個單一表單和一個元件。使用者將輸入值和通知元件開始計算的信號。接著,表單會從元件接收值,並將它們顯示在標籤控制項中。元件會執行需佔用大量處理器的計算,並在計算完成時通知表單。您將在元件中建立公用變數,用來存放從使用者介面接收的值。您也將在您的元件中實作方法來根據這些變數值執行計算。
注意事項 |
---|
函式通常要比方法適合計算值,而引數則無法在執行緒之間傳遞,也無法傳回值。有許多簡單的方法可將值提供給執行緒並從其接收值。在這個範例中,您將會藉由更新公用變數的方式,將值傳回至使用者介面,而且當執行緒執行完成時,將會使用事件來通知主程式。 根據您目前使用的設定或版本,您所看到的對話方塊與功能表指令可能會與 [說明] 中描述的不同。若要變更設定,請從 [工具] 功能表中選擇 [匯入和匯出設定]。如需詳細資訊,請參閱 Visual Studio 設定。 |
若要建立表單
建立新的 [Windows 應用程式] 專案。
將應用程式命名為 Calculations,並將 Form1.vb 重新命名為 frmCalculations.vb。
當 Visual Studio 提示您重新命名 Form1 程式碼項目時,請按一下 [是]。
這個表單將當做應用程式的主要使用者介面。
在表單中加入五個 Label 控制項、四個 Button 控制項以及一個 TextBox 控制項。
控制項
名稱
文字
Label1
lblFactorial1
(空白)
Label2
lblFactorial2
(空白)
Label3
lblAddTwo
(空白)
Label4
lblRunLoops
(空白)
Label5
lblTotalCalculations
(空白)
Button1
btnFactorial1
Factorial
Button2
btnFactorial2
Factorial - 1
Button3
btnAddTwo
Add Two
Button4
btnRunLoops
Run a Loop
TextBox1
txtValue
(空白)
若要建立 Calculator 元件
從 [專案] 功能表中選取 [加入元件]。
將這個元件命名為 Calculator。
若要將公用變數加入至 Calculator 元件
開啟 Calculator 的 [程式碼編輯器]。
加入陳述式以建立公用變數,用來傳遞 frmCalculations 的值至每個執行緒。
變數 varTotalCalculations 將會記錄元件所執行計算的執行總數,其他變數則會接收表單的值。
Public varAddTwo As Integer Public varFact1 As Integer Public varFact2 As Integer Public varLoopValue As Integer Public varTotalCalculations As Double = 0
若要將方法和事件加入至 Calculator 元件
為元件用來將值傳遞至表單的事件宣告委派。在上一個步驟中輸入的變數宣告後面緊接輸入下列程式碼:
Public Event FactorialComplete(ByVal Factorial As Double, ByVal _ TotalCalculations As Double) Public Event FactorialMinusComplete(ByVal Factorial As Double, ByVal _ TotalCalculations As Double) Public Event AddTwoComplete(ByVal Result As Integer, ByVal _ TotalCalculations As Double) Public Event LoopComplete(ByVal TotalCalculations As Double, ByVal _ Counter As Integer)
在步驟 1 中輸入的變數宣告後面,緊接著輸入下列程式碼:
' This sub will calculate the value of a number minus 1 factorial ' (varFact2-1!). Public Sub FactorialMinusOne() Dim varX As Integer = 1 Dim varTotalAsOfNow As Double Dim varResult As Double = 1 ' Performs a factorial calculation on varFact2 - 1. For varX = 1 to varFact2 - 1 varResult *= varX ' Increments varTotalCalculations and keeps track of the current ' total as of this instant. varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next varX ' Signals that the method has completed, and communicates the ' result and a value of total calculations performed up to this ' point RaiseEvent FactorialMinusComplete(varResult, varTotalAsOfNow) End Sub ' This sub will calculate the value of a number factorial (varFact1!). Public Sub Factorial() Dim varX As Integer = 1 Dim varResult As Double = 1 Dim varTotalAsOfNow As Double = 0 For varX = 1 to varFact1 varResult *= varX varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next varX RaiseEvent FactorialComplete(varResult, varTotalAsOfNow) End Sub ' This sub will add two to a number (varAddTwo + 2). Public Sub AddTwo() Dim varResult As Integer Dim varTotalAsOfNow As Double varResult = varAddTwo + 2 varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations RaiseEvent AddTwoComplete(varResult, varTotalAsOfNow) End Sub ' This method will run a loop with a nested loop varLoopValue times. Public Sub RunALoop() Dim varX As Integer Dim varY As Integer Dim varTotalAsOfNow As Double For varX = 1 To varLoopValue ' This nested loop is added solely for the purpose of slowing ' down the program and creating a processor-intensive ' application. For varY = 1 To 500 varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next Next RaiseEvent LoopComplete(varTotalAsOfNow, varX - 1) End Sub
將使用者的輸入傳輸至元件
下一步是將程式碼加入至 frmCalculations 以接收使用者的輸入,以及將值傳送至 Calculator 元件並從其中接收值。
若要將前端功能實作至 frmCalculations
從 [建置] 功能表中選取 [建置方案]。
在 Windows Form 設計工具中開啟表單 frmCalculations。
在 [工具箱] 中找出 [Calculations 元件] 索引標籤。將 [Calculator] 元件拖曳到設計介面。
在 [屬性] 視窗中按一下 [事件] 按鈕。
在四個事件的每一個事件上按兩下,以在 frmCalculations 中建立事件處理常式。在建立每一個事件處理常式後,都需要回到設計工具。
加入下列程式碼,以處理表單自 Calculator1 接收的事件:
Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete lblAddTwo.Text = Result.ToString btnAddTwo.Enabled = True lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete ' Displays the returned value in the appropriate label. lblFactorial1.Text = Factorial.ToString ' Re-enables the button so it can be used again. btnFactorial1.Enabled = True ' Updates the label that displays the total calculations performed lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete lblFactorial2.Text = Factorial.ToString btnFactorial2.Enabled = True lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete btnRunLoops.Enabled = True lblRunLoops.Text = Counter.ToString lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub
在 [程式碼編輯器] 底部找出 End Class 陳述式。就在陳述式上面緊接加入下列陳述式來處理按一下按鈕:
Private Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial1.Click ' Passes the value typed in the txtValue to Calculator.varFact1. Calculator1.varFact1 = CInt(txtValue.Text) ' Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = False Calculator1.Factorial() End Sub Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e _ As System.EventArgs) Handles btnFactorial2.Click Calculator1.varFact2 = CInt(txtValue.Text) btnFactorial2.Enabled = False Calculator1.FactorialMinusOne() End Sub Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnAddTwo.Click Calculator1.varAddTwo = CInt(txtValue.Text) btnAddTwo.Enabled = False Calculator1.AddTwo() End Sub Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnRunLoops.Click Calculator1.varLoopValue = CInt(txtValue.Text) btnRunLoops.Enabled = False ' Lets the user know that a loop is running. lblRunLoops.Text = "Looping" Calculator1.RunALoop() End Sub
測試應用程式
您現在已建立專案,其中包含能夠執行數個複雜計算的表單和元件。雖然您尚未實作多執行緒功能,不過您將在繼續之前先測試您的專案來驗證其功能。
若要測試您的專案
從 [偵錯] 功能表中選擇 [開始偵錯]。接著會啟動應用程式並出現 frmCalculations。
在文字方塊中輸入 4,接著按一下標示為 [Add Two] 的按鈕。
數字 "6" 應該出現在它下方的標籤中,而 "Total Calculations are 1" 則應顯示在 lblTotalCalculations 中。
現在按一下標示為 [Factorial - 1] 的按鈕。
數字 "6" 應該出現在按鈕之下,而 [lblTotalCalculations] 現在則顯示為 "Total Calculations are 4"。
將文字方塊中的值變更為 20,接著按一下標示為 [Factorial] 的按鈕。
數字 "2.43290200817664E+18" 會出現在下面,而 [lblTotalCalculations] 現在則顯示為 "Total Calculations are 24"。
將文字方塊中的值變更為 50000,然後按一下 [Run a Loop] 按鈕。
請注意,在重新啟用這個按鈕之前,會間隔一小段時間。這個按鈕之下的標籤應顯示 "50000",而顯示的總計計算為 "25000024"。
將文字方塊中的值變更為 5000000,然後按一下標示為 [Run A Loop] 的按鈕,緊接著按一下標示為 [Add Two] 的按鈕。再按一下 [Add Two]。
在迴圈完成之前,這個按鈕不會回應,而且表單上的任何控制項也都不會回應。
如果您的程式只執行單一執行緒,那麼像是上面範例中需要大量處理器資源的計算就可能會佔用整個程式,直到計算完成為止。在接下來的章節中,您會將多執行緒功能加入您的應用程式,如此一來多個執行緒就可同時執行。
加入多執行緒功能
先前的範例示範了只執行單一執行緒的應用程式的限制。在接下來的章節中,您將使用 Thread 類別物件,將執行的多個執行緒加入至元件。
若要加入 Threads 副程式
在 [程式碼編輯器] 中開啟 [Calculator.vb]。在接近程式碼最上方之處,找出 Public Class Calculator 這一行。在其下緊接輸入:
' Declares the variables you will use to hold your thread objects. Public FactorialThread As System.Threading.Thread Public FactorialMinusOneThread As System.Threading.Thread Public AddTwoThread As System.Threading.Thread Public LoopThread As System.Threading.Thread
緊接地在程式碼底部的 End Class 陳述式前面加入下列方法:
Public Sub ChooseThreads(ByVal threadNumber As Integer) ' Determines which thread to start based on the value it receives. Select Case threadNumber Case 1 ' Sets the thread using the AddressOf the subroutine where ' the thread will start. FactorialThread = New System.Threading.Thread(AddressOf _ Factorial) ' Starts the thread. FactorialThread.Start() Case 2 FactorialMinusOneThread = New _ System.Threading.Thread(AddressOf FactorialMinusOne) FactorialMinusOneThread.Start() Case 3 AddTwoThread = New System.Threading.Thread(AddressOf AddTwo) AddTwoThread.Start() Case 4 LoopThread = New System.Threading.Thread(AddressOf RunALoop) LoopThread.Start() End Select End Sub
將 Thread 物件具現化 (Instantiated) 時,它需要 ThreadStart 物件形式的引數。ThreadStart 物件是委派,會指向執行緒開始所在的副程式位址。ThreadStart 物件無法使用參數或傳遞值,因此無法指示函式。AddressOf 運算子 (Visual Basic)會傳回當做 ThreadStart 物件的委派。您剛才實作的 ChooseThreads 子函式會從呼叫方法的程式接收值,並使用該值來決定開始的適當執行緒。
若要將執行緒啟動程式碼加入至 frmCalculations
在 [程式碼編輯器] 中開啟 [frmCalculations.vb] 檔案。找出 Sub btnFactorial1_Click。
直接註解呼叫方法 Calculator1.Factorial如下所示的程式碼行:
' Calculator1.Factorial
加入下列程式碼行以呼叫 Calculator1.ChooseThreads 方法:
' Passes the value 1 to Calculator1, thus directing it to start the ' correct thread. Calculator1.ChooseThreads(1)
對其他 button_click 副程式進行類似的修改。
注意事項 請確定 threads 引數包含適當的值。
當您完成時,您的程式碼應該像這樣:
Private Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial1.Click ' Passes the value typed in the txtValue to Calculator.varFact1. Calculator1.varFact1 = CInt(txtValue.Text) ' Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = False ' Calculator1.Factorial() ' Passes the value 1 to Calculator1, thus directing it to start the ' Correct thread. Calculator1.ChooseThreads(1) End Sub Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial2.Click Calculator1.varFact2 = CInt(txtValue.Text) btnFactorial2.Enabled = False ' Calculator1.FactorialMinusOne() Calculator1.ChooseThreads(2) End Sub Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnAddTwo.Click Calculator1.varAddTwo = CInt(txtValue.Text) btnAddTwo.Enabled = False ' Calculator1.AddTwo() Calculator1.ChooseThreads(3) End Sub Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnRunLoops.Click Calculator1.varLoopValue = CInt(txtValue.Text) btnRunLoops.Enabled = False ' Lets the user know that a loop is running. lblRunLoops.Text = "Looping" ' Calculator1.RunALoop() Calculator1.ChooseThreads(4) End Sub
控制項的封送處理呼叫
您現在將加速更新表單上的顯示。由於控制項都是由主執行緒所擁有的,所以從副程式執行緒呼叫控制項都需要「封送處理」呼叫。封送處理 (Marshaling) 是指將呼叫移過執行緒界限的動作,就資源而言,非常昂貴。為將所需封送處理量減至最低,同時為了確定您的呼叫是以安全執行緒的方式處理,您將使用 BeginInvoke 來叫用執行的主執行緒上的方法,藉此將必須進行的跨執行緒界線封送處理量減至最低。當呼叫操作控制項的方法時,這種呼叫是必要的。如需詳細資訊,請參閱 HOW TO:管理執行緒的控制項。
若要建立控制項叫用的程序
為 frmCalculations 開啟 [程式碼編輯器]。在宣告區段中,加入下列程式碼。
Public Delegate Sub FHandler(ByVal Value As Double, ByVal _ Calculations As Double) Public Delegate Sub A2Handler(ByVal Value As Integer, ByVal _ Calculations As Double) Public Delegate Sub LDhandler(ByVal Calculations As Double, ByVal _ Count As Integer)
Invoke 和 BeginInvoke 都需要有適當方法的委派做為引數。這些程式碼行會宣告委派簽章,BeginInvoke 可用它來叫用適當方法。
將下列空白方法加入您的程式碼。
Public Sub FactHandler(ByVal Factorial As Double, ByVal TotalCalculations As _ Double) End Sub Public Sub Fact1Handler(ByVal Factorial As Double, ByVal TotalCalculations As _ Double) End Sub Public Sub Add2Handler(ByVal Result As Integer, ByVal TotalCalculations As _ Double) End Sub Public Sub LDoneHandler(ByVal TotalCalculations As Double, ByVal Counter As _ Integer) End Sub
在 [編輯] 功能表中使用 [剪下] 和 [貼上],將 Sub Calculator1_FactorialComplete 中的所有程式碼剪下,然後貼入 FactHandler。
為 Calculator1_FactorialMinusComplete 和 Fact1Handler、Calculator1_AddTwoComplete 和 Add2Handler,以及 Calculator1_LoopComplete 和 LDoneHandler 重複上一個步驟。
完成時,Calculator1_FactorialComplete、Calculator1_FactorialMinusComplete、Calculator1_AddTwoComplete 和 Calculator1_LoopComplete 中應不會剩下任何程式碼,而曾包含在其中的所有程式碼應都已移至適當的新方法。
呼叫 BeginInvoke 方法,非同步地叫用方法。您可以從表單 (me) 或表單中的任何控制項呼叫 BeginInvoke:
Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete ' BeginInvoke causes asynchronous execution to begin at the address ' specified by the delegate. Simply put, it transfers execution of ' this method back to the main thread. Any parameters required by ' the method contained at the delegate are wrapped in an object and ' passed. Me.BeginInvoke(New FHandler(AddressOf FactHandler), New Object() _ {Factorial, TotalCalculations }) End Sub Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete Me.BeginInvoke(New FHandler(AddressOf Fact1Handler), New Object() _ { Factorial, TotalCalculations }) End Sub Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete Me.BeginInvoke(New A2Handler(AddressOf Add2Handler), New Object() _ { Result, TotalCalculations }) End Sub Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete Me.BeginInvoke(New LDHandler(AddressOf Ldonehandler), New Object() _ { TotalCalculations, Counter }) End Sub
這樣看起來似乎是事件處理常式就只是呼叫下一個方法。實際上,事件處理常式是讓方法在作業的主執行緒上被叫用。這種方法能夠省下跨執行緒界線呼叫,同時允許您的多執行緒應用程式有效執行而不需擔心引發鎖定。如需在多執行緒環境中使用控制項的詳細資訊,請參閱 HOW TO:管理執行緒的控制項.
儲存您的工作。
從 [偵錯] 功能表中選擇 [開始偵錯] 來測試您的方案。
在文字方塊中輸入 10000000,接著按一下 [Run A Loop]。
在這個按鈕下面的標籤中會顯示 "Looping"。這個迴圈在執行時需要相當長的時間。如果太早完成,則請調整數字的大小。
連續快速按一下這三個仍然啟用的按鈕。您會發現所有按鈕都回應您的輸入。[Add Two] 下的標籤應會先顯示結果。稍後結果將會顯示在階乘按鈕之下的標籤中。這些結果會判定為無限,因為 10,000,000 階乘所傳回的數字超過了雙精度變數所能包含的範圍。最後,在稍微延遲之後,結果會傳回 [Run A Loop] 按鈕下方。
就您剛才所見,四組個別的計算是在四個個別執行緒上同時執行。使用者介面仍能回應輸入,而在每個執行緒完成後就會傳回結果。
協調您的執行緒
對多執行緒應用程式熟悉的使用者可能會在剛才輸入的程式碼中發現一個不易察覺的問題。回想一下 Calculator 中每個執行計算的副程式程式碼:
varTotalCalculations += 1
varTotalAsOfNow = varTotalCalculations
上面兩行程式碼會遞增公用變數 varTotalCalculations,並將區域變數 varTotalAsOfNow 設為這個值。然後將這個值傳回至 frmCalculations,並顯示在標籤控制項中。但傳回的是正確的值嗎?如果只執行單一執行緒,那麼答案很顯然是肯定的。但如果執行多個執行緒,則答案就變得不確定。每個執行緒都能夠遞增變數 varTotalCalculations。因此很可能其中一個執行緒累加了這個變數,但是在它將這個值複製到 varTotalAsOfNow 中之前,另一個執行緒又累加一次,因而改變了這個變數的值。這也就是說,每個執行緒實際上都可能報告錯誤的結果。Visual Basic 則提供 SyncLock 陳述式允許執行緒同步,以確保每個執行緒永遠都會傳回正確的結果。SyncLock 的語法如下:
SyncLock AnObject
Insert code that affects the object
Insert some more
Insert even more
' Release the lock
End SyncLock
當輸入 SyncLock 區塊時,特定運算式上的執行會被封鎖,直至指定執行緒在該物件上具有獨佔鎖定。在上述範例中,AnObject 上的執行會停止。SyncLock 必須和會傳回參考 (而非傳回值) 的物件一起使用。接著就可繼續以區塊方式執行而不會干擾其他執行緒。當做一個單位來執行的一組陳述式稱為「原子單位」。當遇到 End SyncLock 時,就會釋出運算式並允許執行緒照常繼續進行。
若要將 SyncLock 陳述式加入至您的應用程式
在 [程式碼編輯器] 中開啟 [Calculator.vb]。
找出下列程式碼的每個執行個體:
varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations
這段程式碼應有四個執行個體,每個計算方法中都有一個。
將這段程式碼修改如下:
SyncLock Me varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations End SyncLock
儲存您的工作並依先前範例中的步驟測試它。
您可能會注意到您的程式效能稍受影響。這是因為當獨佔鎖定您的元件時,執行緒的執行會停止。雖然這能夠確保精確,但這種方法卻會使得多個執行緒的效能好處無法完全發揮。您應仔細考慮鎖定執行緒的需求,同時只在絕對需要時實作它們。