在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。代码如下
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)
Next
Next
End Sub
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
End Function
Private Sub AddListText( ByVal Text As String)
ListBox1.Items.Add(Text)
End Sub
Private Sub _Pool_AllWorkComplete( ByVal Sender As Object, ByVal E As System.EventArgs) Handles _Pool.AllWorkComplete
AddListText( String.Format( "All Work Complete!!!"))
End Sub
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))
End Sub
Private Sub _Pool_WorkStart( ByVal Sender As Object, ByVal E As WorkStartSuspendEventArgs) Handles _Pool.WorkStart
AddListText( String.Format( "Work {0} is Start", E.ID))
End Sub
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 Sub
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日
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 ID As Integer
Public Sub New( ByVal ID As Integer, ByVal Result As Object)
Me.ID = ID
Me.Result = Result
End Sub
End Class
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)
End Sub
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
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
Private Sub RaiseWorkStart( ByVal Worker As BackgroundWorker, ByVal Work As clsWork)
Worker.RunWorkerAsync(Work)
RaiseEvent WorkStart( Me, New WorkStartSuspendEventArgs(Work.ID))
End Sub
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
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
End Class
Public Class WorkStartSuspendEventArgs
Inherits EventArgs
Public ID As Integer
Public Sub New( ByVal ID As Integer)
Me.ID = ID
End Sub
End Class
Public Class WorkCompleteEventArgs
Inherits EventArgs
Public ID As Integer
Public Result As Object
Public Sub New( ByVal ID As Integer, ByVal Result As Object)
Me.ID = ID
Me.Result = Result
End Sub
End Class