在VS中利用BackgroundWorker类来实现“仿线程池”

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
  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日

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 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
posted @ 2011-12-21 15:51  万仓一黍  阅读(2693)  评论(3编辑  收藏  举报