|
实现
我们将在一个类库项目中实现框架,这样就可以在任何需要执行后台处理的应用程序中使用它。
打开Visual Studio .NET并建立一个叫做Background的新类库应用程序。因为该库包含一个Windows窗体控件和窗体,我们需要使用Add References对话框引用System.Windows.Forms.dll和System.Windows.Drawing.dll。此外,我们使用图6中显示的项目属性对话框可以导入这些全项目(project-wide)的名字空间。
 图6:使用项目属性添加全项目名字空间的导入
这完成后我们准备写代码了。从建立接口开始。
定义接口程序
给项目添加一个叫IClient的类,代码如下:
Public Interface IClient
Sub Start(ByVal Controller As Controller)
Sub Display(ByVal Text As String)
Sub Failed(ByVal e As Exception)
Sub Completed(ByVal Cancelled As Boolean)
End Interface |
接着添加一个叫IWorker的类,代码如下:
Public Interface IWorker
Sub Initialize(ByVal Controller As IController)
Sub Start()
End Interface |
最后使用下面的代码添加一个叫IController的类:
Public Interface IController
ReadOnly Property Running() As Boolean
Sub Display(ByVal Text As String)
Sub SetPercent(ByVal Percent As Integer)
Sub Failed(ByVal e As Exception)
Sub Completed(ByVal Cancelled As Boolean)
End Interface |
这时我们已经定义了先前讨论过的类图中的所有接口。因此,现在我们可以实现Controller类。
Controller类
现在我们将实现框架的核心部分--Controller类。该类将包含启动工作线程的代码并在工作线程完成前,作为UI线程和工作线程的中介。
给项目添加一个叫Controller的新类。首先我们将添加一个Imports并声明一些变量:
Imports System.Threading
Public Class Controller
Implements IController
Private mWorker As IWorker
Private mClient As Form
Private mRunning As Boolean
Private mPercent As Integer |
接着我们需要定义一些委托(delegate)。委托是指向方法的形式指针,并且某个方法的委托必须与该方法的特征(参数类型等)相同。
在很多情况中使用委托。在本例中,它们非常重要,因为它们允许一个线程能调用窗体的方法,因此它运行在窗体的UI线程中。IClient所定义的三个窗体的方法都需要委托:
'本委托的特征与IClient.Completed匹配,用于安全地调用UI线程上的法
Private Delegate Sub CompletedDelegate(ByVal Cancelled As Boolean)
'本委托的特征与IClient.Display匹配,用于安全地调用UI线程上的法
Private Delegate Sub DisplayDelegate(ByVal Text As String)
'本委托的特征与IClient.Failed匹配,用于安全地调用UI线程上的法
Private Delegate Sub FailedDelegate(ByVal e As Exception) |
IClient也定义了Start方法,但我们将从UI线程自身中调用它,因此不需要委托。
下一步写将被UI线程调用的代码。该代码包含constructor、Start、Cancel方法和Percent属性。我将这些写入一个区域(Region),可以使它们在UI线程中被调用时比较清晰。
#Region " Code called from UI thread "
' 使用客户(client)初始化controller
Public Sub New(ByVal Client As IClient)
mClient = CType(Client, Form)
End Sub
' 本方法被UI调用并在UI线程上运行。它启动工作线程
Public Sub Start(Optional ByVal Worker As IWorker = Nothing)
' 如果已经运行则产生一个错误信息
If mRunning Then
Throw New Exception("Background process already running")
End If
mRunning = True
' 保存worker对象的指针并初始化该对象,因此它有一个指向Controller的指针
mWorker = Worker
mWorker.Initialize(Me)
'创建后台线程作后台处理
Dim backThread As New Thread(AddressOf mWorker.Start)
'开始后台工作
backThread.Start()
' 告诉客户端后台工作开始了
CType(mClient, IClient).Start(Me)
End Sub
'本方法被UI调用并在UI线程上运行。它仅仅设置一个请求"取消"的标记
Public Sub Cancel()
mRunning = False
End Sub
' 返回完成的百分比值,只被UI线程调用
Public ReadOnly Property Percent() As Integer
Get
Return mPercent
End Get
End Property
#End Region |
唯一特别的代码是在Start方法中我们建立了工作线程然后启动它。
Dim backThread As New Thread(AddressOf mWorker.Start)
backThread.Start() |
为了建立该线程,我们传递了Worker 对象的IWorker 的接口的Start方法的地址。接着我们简单地调用了线程对象的Start方法开始该处理。在这儿我们必须仔细,保证既没有UI与Worker直接交互,也没有Worker与UI直接交互。
注意"取消"方法只是设置了一个标志用于显示我们想工作不再运行。直到工作代码周期性查看是否该停止运行。
现在我们实现在Worker对象运行时将被工作线程调用的代码。该代码有趣一些,它将工作线程调用的Display和Completed方法传递到UI,但却在UI线程上。
为了实现它,我们使用窗体对象的Invoke方法。Invoke方法接受指向该窗体将调用的方法的委托,同时有一个含有该方法的参数的类型对象数组。
Invoke方法不能直接调用窗体的方法。它请求窗体转向并使用该窗体的UI线程来调用方法。这通过发送一个Windows消息给窗体在后台实现。这意味着窗体获取这些方法调用与它从操作系统本身获取click或 keypress事件非常相象。
典型情况下这些并不麻烦。其结果是Invoke方法触发一个进程,通过该进程窗体终止在自己的UI线程上运行的方法,这正是我们的设计目标。
同样这些代码写入一个区域(Region),可以使它们在worker线程中被调用时比较清晰。
#Region " Code called from the worker thread "
'从worker线程调用来更新显示
'它用状态信息触发了一个向UI的方法调用
'该调用在UI线程上作出
Private Sub Display(ByVal Text As String) _
Implements IController.Display
Dim disp As New DisplayDelegate( _
AddressOf CType(mClient, IClient).Display)
Dim ar() As Object = {Text}
'调用UI线程的客户窗体来更新显示
mClient.BeginInvoke(disp, ar)
End Sub
'从worker线程调用用于显示失败。
'它用exception对象触发了一个向UI的方法调用
'该调用在UI线程上作出
Private Sub Failed(ByVal e As Exception) _
Implements IController.Failed
Dim disp As New FailedDelegate(_
AddressOf CType(mClient, IClient).Failed)
Dim ar() As Object = {e}
'调用UI线程上的客户窗体来显示失败
mClient.Invoke(disp, ar)
End Sub
'从worker线程调用来表明完成百分比
'该值进入Controller,如果需要能在那儿被UI读取
Private Sub SetPercent(ByVal Percent As Integer) _
Implements IController.SetPercent
mPercent = Percent
End Sub
'从worker线程调用来显示已经完成
'传递了一个参数用于显示真的完成了和者"取消"了
'该调用在UI线程中作出
Private Sub Completed(ByVal Cancelled As Boolean) _
Implements IController.Completed
mRunning = False
Dim comp As New CompletedDelegate( _
AddressOf CType(mClient, IClient).Completed)
Dim ar() As Object = {Cancelled}
'从UI线程调用客户窗体来显示完成了
mClient.Invoke(comp, ar)
End Sub
'显示是否仍在运行或者有"取消"请求
'该调用在worker线程上作出,这样worker代码可以看是否应该温和地退出
Private ReadOnly Property Running() As Boolean _
Implements IController.Running
Get
Return mRunning
End Get
End Property
#End Region
Failed和Completed方法使用了窗体的Invoke方法。下面使Failed的代码:
Dim disp As New FailedDelegate(_
AddressOf CType(mClient, IClient).Failed)
Dim ar() As Object = {e}
'从UI线程调用客户窗体来显示失败
mClient.Invoke(disp, ar) |
首先所我们从IClient接口中建立一个委托指向窗体的Failed事件,接着我们声明了一个类型对象数组存放传递给该方法的参数值,最后调用客户窗体的Invoke方法,传递委托指针和参数数组给窗体。
随后窗体在UI线程中使用这些参数调用该方法,它可以安全的运行来更新显示。
这整个过程是同步的,意味着向窗体作调用时工作线程被阻塞。虽然因为错误消息或者完成消息而阻塞工作线程是合适的,但我们不想因为一点点状态显示而阻塞它。
为了避免在状态显示时的阻塞,Display方法用BeginInvoke代替了Invoke. BeginInvoke促成窗体上的方法调用异步完成,这样工作线程能保持运行而不需要等待窗体显示方法的完成。
Dim disp As New DisplayDelegate( _
AddressOf CType(mClient, IClient).Display)
Dim ar() As Object = {Text}
'调用UI线程上的客户窗体来更新显示
mClient.BeginInvoke(disp, ar) |
在这种情况下使用BeginInvoke通过避免阻塞保持了工作线程尽可能地高效率运行。
ActivityBar控件
最后我们建立ActivityBar控件来显示动态点。
给项目添加一个叫ActivityBar的用户控件,将该控件调整到大约宽110,高20,可以拖拉边界或在属性窗口中设定Size值来实现。
其它的在代码中实现。为了建立一系列动态闪烁的"亮点",我们将Timer控件与一组PictureBox控件一起使用。每次Timer控件到期,我们将下一个PictureBox设为绿色,将已经是绿色的变成窗体色。
放置Timer控件,将它的名字改为tmAnim,Interval属性设为300。
另外,在Components页上有一个不同的Timer控件。这是个多线程时钟。换句话说,它在后台线程上建立Elapsed事件,不同于UI线程上的Windows窗体时钟。在建立UI时这明显达不到目标,因为Elapsed事件中的代码明显不能直接与UI交互。
现在向控件中添加下列代码:
Private mBoxes As New ArrayList()
Private mCount As Integer
Private Sub ActivityBar_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Dim index As Integer
If mBoxes.Count = 0 Then
For index = 0 To 6
mBoxes.Add(CreateBox(index))
Next
End If
mCount = 0
End Sub
Private Function CreateBox(ByVal index As Integer) As PictureBox
Dim box As New PictureBox()
With box
SetPosition(box, index)
.BorderStyle = BorderStyle.Fixed3D
.Parent = Me
.Visible = True
End With
Return box
End Function
Private Sub GrayDisplay()
Dim index As Integer
For index = 0 To 6
CType(mBoxes(index), PictureBox).BackColor = Me.BackColor
Next
End Sub
Private Sub SetPosition(ByVal Box As PictureBox, ByVal Index As Integer)
Dim left As Integer = CInt(Me.Width / 2 - 7 * 14 / 2)
Dim top As Integer = CInt(Me.Height / 2 - 5)
With Box
.Height = 10
.Width = 10
.Top = top
.Left = left + Index * 14
End With
End Sub
Private Sub tmAnim_Tick(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles tmAnim.Tick
CType(mBoxes((mCount + 1) Mod 7), PictureBox).BackColor = _
Color.LightGreen
CType(mBoxes(mCount Mod 7), PictureBox).BackColor = Me.BackColor
mCount += 1
If mCount > 6 Then mCount = 0
End Sub
Public Sub Start()
CType(mBoxes(0), PictureBox).BackColor = Color.LightGreen
tmAnim.Enabled = True
End Sub
Public Sub [Stop]()
tmAnim.Enabled = False
GrayDisplay()
End Sub
Private Sub ActivityBar_Resize(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles MyBase.Resize
Dim index As Integer
For index = 0 To mBoxes.Count - 1
SetPosition(CType(mBoxes(index), PictureBox), index)
Next
End Sub |
窗体的Load事件建立PictureBox控件并把它们放入一个数组,这样容易循环。Timer控件的Tick事件按次序循环使每个点变为绿色。
这些都由Start事件启动,由Stop事件停止。由于Stop是保留字,该方法的名称加上了方括号:[Stop]。Stop方法不仅停止定时器,而且使所有的方框变为灰色以显示没有当前活动。
建立Worker
前面我们看到了一个简单的Worker类。我们已经定义IWorker接口,现在能利用已经建立的Controller来增强该类。
首先建立Background.dll文件。这一步很重要,如果没有的话,我们在建立测试窗体时,ActivityBar控件不会在Toolbox上出现。
给解决方案添加一个叫bgTest的Windows窗体应用程序项目,将它设置为启动项目。
接着使用Add References 对话框的 Projects页来添加对Background项目的引用。
Imports Background
Public Class Worker Implements IWorker Private mController As IController Private mInner As Integer Private mOuter As Integer Public Sub New(ByVal InnerSize As Integer, ByVal OuterSize As Integer) mInner = InnerSize mOuter = OuterSize End Sub '被controller调用,这样可以得到一个controller的指针。
Private Sub Init(ByVal Controller As IController) _ Implements IWorker.Initialize mController = Controller End Sub
Private Sub Work() Implements IWorker.Start Dim innerIndex As Integer Dim outerIndex As Integer Dim value As Double Try For outerIndex = 0 To mOuter If mController.Running Then mController.Display("Outer loop " & outerIndex & " starting") mController.SetPercent(CInt(outerIndex / mOuter * 100)) Else '有"取消"请求 mController.Completed(True) Exit Sub End If
For innerIndex = 0 To mInner '在此处作一些cool运算 value = Math.Sqrt(CDbl(innerIndex - outerIndex)) Next Next mController.SetPercent(100) mController.Completed(False) Catch e As Exception mController.Failed(e) End Try
End Sub
End Class
|
我们添加了Init方法来执行IWorker.Initialize。Controller调用该方法,这样就有了一个Controller对象的指针。
我们将Work方法改为私有(Private),仅仅用于执行IWorker.Start方法。该方法将在工作线程上运行。
Work方法使用Try..Catch块得到了加强,这样我们能捕捉任何错误并使用Controller的Failed方法将错误返回到UI。
假定代码能运行,在该代码运行时,我们调用Controller对象的Display和SetPercent方法来更新状态和完成百分比。
我们也周期性地检查Controller对象的Running属性来查看是否有"取消"请求。如果有,就停止处理并显示由于有"取消"请求而完成。
建立显示窗体
最后建立一个窗体来启动和取消后台处理。它也显示活动和状态信息。
给窗体添加两个按钮(btnStart和btnRequestCancel)、两个label(Label1和Label2)、一个ProgressBar(ProgressBar1)和ActivityBar (ActivityBar1),如图7所示:
 图7:Form1的布局
窗体需要执行IClient,这样Controller控件才能与它交互。
Imports Background Public Class Form1 Inherits System.Windows.Forms.Form Implements IClient窗体也需要一个Controller对象和一个标志来追踪后台处理是活动的或完成了。
Private mController As New Controller(Me) Private mActive As Boolean
接着我们添加方法来实现IClient中定义的接口。
#Region " IClient "
Private Sub TaskStarted(ByVal Controller As Controller) _ Implements IClient.Start
mActive = True
Label1.Text = "Starting" Label2.Text = "0%" ProgressBar1.Value = 0 ActivityBar1.Start() End Sub
Private Sub TaskStatus(ByVal Text As String) _ Implements IClient.Display
Label1.Text = Text Label2.Text = CStr(mController.Percent) & "%" ProgressBar1.Value = mController.Percent
End Sub
Private Sub TaskFailed(ByVal e As Exception) _ Implements IClient.Failed
ActivityBar1.Stop()
Label1.Text = e.Message MsgBox(e.ToString) mActive = False
End Sub
Private Sub TaskCompleted(ByVal Cancelled As Boolean) _ Implements IClient.Completed
Label1.Text = "Completed" Label2.Text = CStr(mController.Percent) & "%" ProgressBar1.Value = mController.Percent ActivityBar1.Stop() mActive = False End Sub #End Region
|
注意所有的代码都没有处理线程。每部分都包含监视后台处理的状态时可以作适当的响应的代码。每次我们都更新过程状态信息的显示、它的完成百分比(在文本框和ProgressBar中)并启动和停止ActivityBar控件。
标志mActive很重要。当工作线程活动时,如果用户关闭了窗体,程序可能挂起或者不稳定。为了避免这种情况,如果后台处理是激活的,我们截取窗体的Closing事件并在后台处理活动时终止关闭的企图。
Private Sub Form1_Closing(ByVal sender As Object, _ ByVal e As System.ComponentModel.CancelEventArgs) _ Handles MyBase.Closing e.Cancel = mActive End Sub |
在本例中我们选择初始化"取消"操作,这依赖于具体的应用程序需求。
下面的代码用于实现按钮的Click事件:
Private Sub btnStart_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnStart.Click mController.Start(New Worker(2000000, 100)) End Sub
Private Sub btnStop_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnStop.Click
Label1.Text = "Cancelling ..." mController.Cancel() End SubStart |
按钮简单地调用Controller的Start方法,将Worker对象的一个实例传递给它。
为了使它运行得更有趣,你也许要调整初始化Worker对象得值。本文中的值在两个P3/450得计算机上运行很好。实际的Worker对象执行更有意思的工作,但也是长时间的处理。
Cancel按钮调用Controller对象Cancel方法,并更新显示来表明有"取消"请求。这仅仅是个"取消"请求,在工作实际停止前也许有一段时间。比较好的方法是给用户一些回应,至少表明点击按钮已经被注意到了。
运行该程序。Start按钮按下时,Worker启动了,随着它的运行显示在发生变化。你可以在屏幕上移动窗体并与窗体交互,因为UI线程本质上是空闲的,准备好了与你交互。
同时,工作线程正在后台处理繁忙工作,给UI线程发送周期性的状态更新消息以供显示。
结论
多线程是个非常强大的工具,在任何需要长时间运行的事务中都能使用。我们可以用它运行工作代码而不停止用户界面。同时,多线程的使用可能变得难以想象的复杂,更加难于调试。
但这不总是可行的,我们应该力争给每个工作线程提供一组该线程操作的独立数据。最简单的实现方法是为每个线程创建一个对象,该对象包含线程需要操作的数据和工作的代码。
通过实现一个结构化的框架作为工作线程与UI线程的中介,我们难以想像地简化了编写多线程工作代码和控制它的UI代码。本文中我有一个框架实例,你能改变它使之适应你的应用程序需求。
|