在VS編程中,一般遇到比較耗時的操作的時候(例如:從網絡上下載文檔,文件的IO操作等),如果采用一般的做法,主線程會一直等待操作完成,會遇到界面假死的問題。故在此情況下,合理的做法是采用異步操作和多線程操作。異步操作可以在另開一個線程執行耗時的操作,在主線程上是不等返回,直接操作下一步,從而解決了界面假死的情況。不過,由於異步操作是新開了一個線程,在新開的線程裡操作界面元素的時候(例如:在下載文檔時顯示進度,修改界面上的進度條的數值),會拋出一個線程安全的異常。為了解決這個問題,VS提供了BackgroundWorker類,通過內部封裝,提供了一個異步操作,同時又能解決線程安全的問題。
BackgroundWorker類提供了二個方法和三個事件來實現異步操作的線程安全的問題。
首先是RunWorkerAsync方法,告訴系統現在要新開一個線程來執行一個異步操作。該方法會引發DoWork事件,在DoWork事件內,執行一個耗時的操作。此時,該事件中的代碼是執行在另一個線程中。如果在該事件中,嘗試操作主界面上的元素的時候,立馬拋出一個線程安全的異常。
那該如何操作主界面的元素呢?在DoWork事件中調用ReportProgress方法,引發ProgressChanged事件,並通過userState參數把參數傳遞過去。Progresschanged事件是和主界面在一個線程裡。在該事件裡,根據傳遞來的參數操作主界面上的元素就不會有線程安全的問題。
在執行完DoWork事件中的代碼後,會調用RunWorkerCompleted事件,通知主線程,異步操作已經完成。同樣該事件也是和主界面在同一個線程裡,也同樣能操作主界面的元素而不會引發線程安全的異常。
如果現在有一個任務是下載200個網頁。該如何操作?一個接著一個下載,利用類可以解決界面假死和線程安全的問題。不過效率也太低了一點。如果利用多線程同時下載200個網頁,那麼可能超過系統的負擔,造成效率的低下。
“線程池”的概念應運而生。在線程池裡准備好一定數量的線程,例如50個線程。以上面的例子,200個下載網頁任務。由於只有50個線程。那麼50個下載網頁任務先執行,剩下的150個下載網頁任務先暫時掛起。等到某一個線程執行完任務後,再執行掛起的任務。直到所有的任務都完成。“線程池”的好處是嚴格控制線程的數量,不給系統造成太大的負擔。
根據“線程池”的思想。自己編寫了一個類。類的全部代碼附在本文的最後。
接下來,闡述一下該類的具體實現。類的名稱為clsWorkPool
首先,clsWorkPool類定義一個委托,該委托來完成“工作”。該類只負責“線程池”的實現與調度,不實現具體的工作。故用委托比較合適。委托的定義如下:
Public Delegate Function WorkDelegate(ByVal Param As Object) As Object
類clsWorkPool的構造函數代碼如下
Public Sub New(ByVal ThreadCount As Integer)
_ThreadCount = ThreadCount
ReDim _BgWorker(ThreadCount - 1)
Dim I As Integer
For I = 0 To ThreadCount - 1
_BgWorker(I) = New BackgroundWorker
AddHandler _BgWorker(I).DoWork, AddressOf RunWorkerStart
AddHandler _BgWorker(I).RunWorkerCompleted, AddressOf RunWorkerCompleted
Next
_Work = New Queue(Of clsWork)
_ID = 0
_HadComplete=0
_ThreadLock = New Object
End Sub
根據傳遞進來的參數_ThreadCount,來創立“線程池”——BackgroundWorker類的數組。該數組內的數量決定了線程池中線程的數量。_Work是一個隊列對象,將暫時不能執行的任務,掛起到隊列中,等到有空閒的線程的時候再執行。_ThreadLock是一個線程安全鎖。防止多線程操作,修改參數,互相影響。
類clsWorkPool的添加任務的代碼
Public Function DoWork(ByVal Work As WorkDelegate, ByVal Param As Object) As Integer
SyncLock _ThreadLock
Dim I As Integer, J As Boolean
_ID += 1
J = False
Dim tWork As New clsWork(Work, Param, _ID)
For I = 0 To _ThreadCount - 1
If _BgWorker(I).IsBusy = False Then
RaiseWorkStart(_BgWorker(I), tWork)
J = True
Exit For
End If
Next
If J = False Then
_Work.Enqueue(tWork)
RaiseEvent WorkSuspend(Me, New WorkStartSuspendEventArgs(_ID))
End If
DoWork = _ID
End SyncLock
End Function
由於牽涉到多線程異步操作,故在代碼的開始和結束添加線程鎖。首先,根據傳遞進來的參數,生成一個包含任務各種參數的一個類clsWork。然後遍歷線程池,看有沒有空閒的線程。如果有空閒的線程,調用RaiseWorkStart(_BgWorker(I), tWork)方法,通過空閒的線程來完成任務。在RaiseWorkStart(_BgWorker(I), tWork)方法中,有兩句話,一是調用BackgroundWorker類的實例Work的RunWorkerAsync方法,啟用輔助線程完成工作;一是引發WorkStart事件,通知主線程該工作已經啟動。如果沒有空閒的線程,則將該任務添加到隊列_Work中,等待空閒的線程,並引發WorkSuspend事件,通知主線程該工作暫時掛起。
在調用Work的RunWorkerAsync方法之後,會引發Work的DoWork的事件,即下面的RunWorkerStart方法,通過調用clsWork類的Work委托的Invoke方法,來完成該任務,並將返回值寫回。
Private Sub RunWorkerStart(ByVal sender As Object, ByVal e As DoWorkEventArgs)
Dim T As clsWork = CType(e.Argument, clsWork)
e.Result = New clsResult(T.ID, T.Work.Invoke(T.Param))
End Sub
在執行完上面的函數,會引發Work的RunWorkerCompleted事件,即下面的RunWorkerCompleted方法。
Private Sub RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
SyncLock _ThreadLock
Dim T As clsResult = CType(e.Result, clsResult)
RaiseEvent WorkComplete(Me, New WorkCompleteEventArgs(T.ID, T.Result))
_HadComplete += 1
If _Work.Count > 0 Then
Dim tW As BackgroundWorker = CType(sender, BackgroundWorker)
If tW.IsBusy = False Then RaiseWorkStart(tW, _Work.Dequeue)
Else
If _HadComplete >= _ID Then RaiseEvent AllWorkComplete(Me, New EventArgs)
End If
End SyncLock
End Sub
首先引發WorkComplete事件,告訴主線程,該任務已經完成。將完成的任務數加1。同時,檢查掛起的任務數,若還有掛起的任務,則調用RaiseWorkStart方法,重新啟動隊列中的一個新的任務。若沒有掛起的任務,則檢查完成的任務數,任務數達到一定的數量,則說明所有的任務都完成了,則引發AllWorkComplete事件。告知主線程,所有的任務都已經完成。
下面舉一個例子,來展示該類的實際效果
在Form上,放一個ListBox和Button。代碼如下
Public Class Form1
在按下Button1之後,先初始化線程池中5個線程。然後添加了48個下載網頁任務,每個任務調用GetWebString函數,該函數符合Work的委托。由於只有5個線程,故有43個線程被掛起,直到有任務完成後,再執行掛起的任務。
下面,貼二張截圖