在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))
在執行完上面的函數,會引發Work的RunWorkerCompleted事件,即下面的RunWorkerCompleted方法。
Private Sub RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
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)
首先引發WorkComplete事件,告訴主線程,該任務已經完成。将完成的任務數加1。同時,檢查挂起的任務數,若還有挂起的任務,則調用RaiseWorkStart方法,重新啟動隊列中的一個新的任務。若沒有挂起的任務,則檢查完成的任務數,任務數達到一定的數量,則說明所有的任務都完成了,則引發AllWorkComplete事件。告知主線程,所有的任務都已經完成。
下面舉一個例子,來展示該類的實際效果
在Form上,放一個ListBox和Button。代碼如下
Public Class Form1
Private WithEvents _Pool As clsWorkPool
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
_Pool = New clsWorkPool(5)
Dim I As Integer, J As Integer
For I = 2008 To 2011
For J = 1 To 12
_Pool.DoWork(AddressOf GetWebString, I & "-" & J)
Public Function GetWebString(ByVal Url As Object) As Object
Dim _Web As New Net.WebClient
Dim _UrlParam() As String = CType(Url, String).Split("-")
Dim _Url As String = String.Format("http://www.istartedsomething.com/bingimages/?m={0}&y={1}", _UrlParam(0), _UrlParam(1))
Dim str As IO.Stream
str = _Web.OpenRead(_Url)
Dim read As New IO.StreamReader(str, System.Text.Encoding.GetEncoding("GB2312"))
Dim Text As String = read.ReadToEnd()
Return Url
Private Sub AddListText(ByVal Text As String)
ListBox1.Items.Add(Text)
Private Sub _Pool_AllWorkComplete(ByVal Sender As Object, ByVal E As System.EventArgs) Handles _Pool.AllWorkComplete
AddListText(String.Format("All Work Complete!!!"))
Private Sub _Pool_WorkComplete(ByVal Sender As Object, ByVal E As WorkCompleteEventArgs) Handles _Pool.WorkComplete
AddListText(String.Format("Work {0} is Complete,The result is {1}", E.ID, E.Result.ToString))
Private Sub _Pool_WorkStart(ByVal Sender As Object, ByVal E As WorkStartSuspendEventArgs) Handles _Pool.WorkStart
AddListText(String.Format("Work {0} is Start", E.ID))
Private Sub _Pool_WorkSuspend(ByVal Sender As Object, ByVal E As WorkStartSuspendEventArgs) Handles _Pool.WorkSuspend
AddListText(String.Format("Work {0} is Suspend", E.ID))
End Class
在按下Button1之後,先初始化線程池中5個線程。然後添加了48個下載下傳網頁任務,每個任務調用GetWebString函數,該函數符合Work的委托。由于隻有5個線程,故有43個線程被挂起,直到有任務完成後,再執行挂起的任務。
下面,貼二張截圖
通過修改線程池的線程數,發現,在不同的線程數下,效果不完全一樣。線程為1的時候,此時隻有一個輔助線程,和單線程無異,完成48個任務一共耗時88.6秒。線程數為20的時候,效果比較好,完成48個任務一共耗時26.2秒。線程數為50的時候,此時,所有的任務都沒有挂起,直接運作。完成這些任務,一共耗時44.1秒,反而不如20個線程的時候,可見,在下載下傳的任務的時候時,線程數不宜過多。
還有一點說明的是,在例子中,48個任務執行的是同一種任務——調用的同一個函數。實際情況是可以調用不同任務——調用不同的函數,隻要這些函數滿足同一種委托。
附:“線程池”的全部代碼。代碼格式修正于2012年1月6日
Imports System.ComponentModel
Public Class clsWorkPool
Public Delegate Function WorkDelegate(ByVal Param As Object) As Object
Private Class clsWork
Public Work As WorkDelegate
Public Param As Object
Public ID As Integer
Public Sub New(ByVal Work As WorkDelegate, ByVal Param As Object, ByVal ID As Integer)
Me.Work = Work
Me.Param = Param
Me.ID = ID
End Sub
End Class
Private Class clsResult
Public Result As Object
Public Sub New(ByVal ID As Integer, ByVal Result As Object)
Me.Result = Result
Private _ThreadCount As Integer
Private _BgWorker() As BackgroundWorker
Private _ID As Integer
Private _HadComplete As Integer
Private _Work As Queue(Of clsWork)
Private _ThreadLock As Object
Public Event WorkStart(ByVal Sender As Object, ByVal E As WorkStartSuspendEventArgs)
Public Event WorkComplete(ByVal Sender As Object, ByVal E As WorkCompleteEventArgs)
Public Event WorkSuspend(ByVal Sender As Object, ByVal E As WorkStartSuspendEventArgs)
Public Event AllWorkComplete(ByVal Sender As Object, ByVal E As EventArgs)
Public Sub New()
Me.New(50)
Public Sub New(ByVal ThreadCount As Integer)
_HadComplete = 0
Private Sub RaiseWorkStart(ByVal Worker As BackgroundWorker, ByVal Work As clsWork)
Worker.RunWorkerAsync(Work)
RaiseEvent WorkStart(Me, New WorkStartSuspendEventArgs(Work.ID))
Private Sub RunWorkerCompleted(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs)
If _HadComplete >= _ID Then RaiseEvent AllWorkComplete(Me, New EventArgs)
Public Class WorkStartSuspendEventArgs
Inherits EventArgs
Public ID As Integer
Public Sub New(ByVal ID As Integer)
Me.ID = ID
Public Class WorkCompleteEventArgs
Public Result As Object
Public Sub New(ByVal ID As Integer, ByVal Result As Object)
Me.Result = Result