E N D
第19章 编写具有多个线程运行的应用程序 对于比较复杂的应用程序来说,同时执行多个任务的能力通常是一个关键性的特性。在此之前,我们看到的应用程序均属于同步应用程序。尽管同步应用程序易于开发,但是它们的性能通常比多线程应用程序低,因为一个新的事务必须等待前面的事务完成后才能开始。如果完成某个同步事务的时间比预想的要长,应用程序可能没有响应。而多线程处理可以同时运行多个过程。例如,字处理程序能够在继续操作文档的同时执行拼写检查事务。.NET Framework提供的线程库能够让开发人员自由创建完全线程化的应用程序,整个程序都可以异步执行,从而允许更高效的设计和更高的交互响应性。本章介绍VB.NET中的多线程技术。
章节内容 • 19.1 了解线程的基本概念 • 19.2 线程创建与管理 • 19.3 使用多线程组件轻松创建多线程应用程序
19.1 了解线程的基本概念19.1.1 进程、线程和应用程序域 • 进程 • 应用程序在计算机上是以单独的进程运行的,每一个新启动的应用程序都会创建一个新的进程。 • 一个进程至少包含一个执行线程并有它自己的地址空间,其目的是将应用程序相互隔离,因为进程之间不能直接共享内存。
线程 • 线程是操作系统分配处理器时间的基本单位。操作系统将运行时间分配给线程,而不是进程。 • 线程不能单独存在,要求拥有一个所有者进程。而一个进程可以包含至少一个线程,即主进程线程(Sub Main),在主进程线程退出时该进程被终止。 • 线程可以与同一进程中的其他线程共享内存 • 线程还有一组关联的资源,包括它自己的异常处理程序、任务计划优先级和操作系统在给其他线程分配运行时间时保留的该线程的上下文信息。
应用程序域 • 应用程序域提供安全而通用的处理单元,公共语言运行库可使用它来提供应用程序之间的隔离。 • 可以在单个进程中运行几个应用程序域,而不会造成进程间调用或进程间切换等方面的额外开销。
19.1.2 线程限制 • 任何操作系统对于每个进程上的线程数量均有一个限制。因为每个线程都需要消耗真实的物理资源,保存线程上下文信息需要占用内存。 • 下述情况最适合采用多线程技术开发。 • 时间密集或处理密集的事务妨碍用户界面 • 单独的事务必须等待外部资源,例如远程文件或Internet连接。
19.2 线程创建与管理19.2.1 创建线程 • 使用System.Threading.Thread类 • 下面代码创建了一个新的线程,并在该线程上运行名为myTask的子过程。 1 Dim myThread As New System.Threading.Thread(AddressOf myTask) 2 myThread.Start( ) 3 '这里的代码马上执行
安全点Safe Point • 安全点是指代码中公共语言运行库可以安全地执行自动垃圾回收的位置;垃圾回收是指释放不再使用的变量并回收内存的过程。 • 调用线程的Abort或Suspend方法时,公共语言运行库将对代码进行分析,确定让线程停止运行的适当位置。
线程状态 • 线程从创建到终止,一定处于某种状态中。 • 线程状态是由System.Threading.Thread.ThreadState属性来定义的。
通过执行Thread类提供的用于控制单个线程的方法,线程的状态也会随之改变。通过执行Thread类提供的用于控制单个线程的方法,线程的状态也会随之改变。
19.3 使用多线程组件轻松创建多线程应用程序 • BackGroundWork • Timer组件
19.3.1 BackGroundWorker组件 • 许多常执行的操作可能需要比较长的操作时间,这样的操作可能会导致用户界面在运行时挂起,要等待操作完成后才可以进行其他操作。长时间的等待延迟对用户来说是不可忍受的。 • BackGroundWorker组件为该类问题提供了一个解决方案,即将耗时操作在不同于应用程序的主用户界面线程的另一线程上异步执行,也就是所谓的在后台执行。
示例:计算Fibonacci数 • 界面设计:
下面代码是窗体的Load事件过程,实现控件的初始化。下面代码是窗体的Load事件过程,实现控件的初始化。 • 1 Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load • 2 lblMessage.Text = "" • 3 ProgressBar1.Visible = False • 4 btnCancel.Enabled = False • 5 BackgroundWorker1.WorkerReportsProgress = True • 6 BackgroundWorker1.WorkerSupportsCancellation = True • End Sub • 单击【开始】按钮后,开始在另外一个线程上执行计算Fibonacci数的操作。
下面代码是【开始】按钮的Click事件过程。 • 1 Private Sub btnStart_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStart.Click • 2 txbNum.Enabled = False • 3 btnStart.Enabled = False • 4 btnCancel.Enabled = True • 5 lblMessage.Text = "请稍后……" • 6 ProgressBar1.Visible = True • 7 numberToCompute = CInt(txbNum.Text) • 8 ' 调用异步操作方法 • 9 BackgroundWorker1.RunWorkerAsync(numberToCompute) • End Sub • 第9行调用BackGroundWorker组件的RunWorkerAsync方法,表示开始一个异步操作,此方法触发BackGroundWorker组件的DoWork事件。这意味着,在本例中,计算Fibonacci数的代码需要放在DoWork事件的处理过程内。
下面代码是BackGroundWorker组件的DoWork事件过程。下面代码是BackGroundWorker组件的DoWork事件过程。 1 Private numberToCompute As Integer = 0 2 Private highestPercentageReached As Integer = 0 3 4 Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork 5 ' 得到触发该事件的BackGroundWorker的实例 6 Dim worker As BackgroundWorker = CType(sender, BackgroundWorker) 7 ' 将计算结果赋值给DoWorkEventArgs的Result属性。该属性在RunWorkerCompleted的事件处理过程中可见 8 e.Result = ComputeFibonacci(e.Argument, worker, e) 9 End Sub
下面代码是ComputeFibonacci函数。 1 ' ComputeFibonacci函数的功能是返回第n项的Fibonacci数 2 Function ComputeFibonacci(ByVal n As Integer, ByVal worker As BackgroundWorker, ByVal e As DoWorkEventArgs) As Long 3 ' 参数n必须在0到91之间,因为第92项Fibonacci数超出了Long型的表示范围 4 If n < 0 OrElse n > 91 Then 5 Return -1 6 End If 7 '定义结果变量 8 Dim result As Long = 0 9 ' 如果用户选择取消操作,即调用了CancelAsync方法,此时CancellationPending属性设置为true。 10 ' 因此可以根据CancellationPending属性来判断用于是否取消了操作。之后将DoWorkEventArgs.Cancel标志设为True。
11 If worker.CancellationPending Then 12 e.Cancel = True 13 ' 如果没有取消,则进行递归计算 14 Else 15 If n < 2 Then 16 result = 1 17 Else 18 result = ComputeFibonacci(n - 1, worker, e) + ComputeFibonacci(n - 2, worker, e) 19 End If 20 ' 报告计算进度 21 Dim percentComplete As Integer = CSng(n) / CSng(numberToCompute) * 100 22 If percentComplete > highestPercentageReached Then 23 highestPercentageReached = percentComplete 24 worker.ReportProgress(percentComplete) 25 End If 26 End If 27 ' 返回计算结果 28 Return result 29 End Function
下面代码是BackgroundWorker控件的ProgressChanged事件处理过程。下面代码是BackgroundWorker控件的ProgressChanged事件处理过程。 1 Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged 2 ' 设置进度条 3 ProgressBar1.Value = e.ProgressPercentage 4 End Sub
下面代码是RunWorkerCompleted事件的处理过程。 1 Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted 2 If (e.Error IsNot Nothing) Then 3 ' 出现异常的情况 4 lblMessage.Text = e.Error.Message 5 ' 被取消的情况 6 ElseIf e.Cancelled Then 7 lblMessage.Text = "任务已取消!" 8 ElseIf e.Result = -1 Then 9 ' 参数超出范围的情况 10 lblMessage.Text = "请输入在0到91之间的整数!" 11 Else 12 ' 任务正常完成的情况 13 lblMessage.Text = "已完成!结果为" & e.Result.ToString() 14 End If 15 txbNum.Enabled = True 16 btnStart.Enabled = True 17 btnCancel.Enabled = False 18 ProgressBar1.Visible = False 19 End Sub
分析: • 由于任务发生异常、被取消或者正常完成,均会触发RunWorkerCompleted事件,因此在该事件的处理过程中,需要区分三种情况:如果出现异常,则RunWorkerCompletedEventArgs类型的参数e的Error属性包含了异常的信息;如果被取消,则e的Cancelled属性被设置为True;如果正常完成,则e的Result属性包含了计算结果。根据不同的情况分别设置lblMessage控件的Text属性以完成结果显示。
19.3.2 Timer组件 • 该Timer组件则位于System.Timers 命名空间,称为服务器计时器。 • 位于工具箱组件选项卡中的Time组件是一个实现按用户定义的时间间隔引发事件的计时器,位于System.Windows.Forms命名空间,是一个专为单线程设计的计时器,称为Windows计时器。
二者区别 • 服务器计时器是为在多线程环境下与辅助线程一起使用而设计的。由于二者使用不同的体系结构,因此基于服务器的计时器可能比 Windows 计时器精确得多。 • 服务器计时器可以在线程之间移动来处理引发的事件。
创建 Timer 组件的实例 • 两种方法: • 一是将其添加到工具箱中; • 二是通过代码创建Timer组件的实例。
第一种方法 • 在【工具】菜单上单击【选择工具箱项】,在打开的【选择工具箱项】窗口中再单击【.NET Framework组件】选项卡, ,勾选【System.Timers】命名空间中的【Timer】复选框。
之后,直接将Timer图标拖动到窗体上,即可为该窗体添加一个服务器计时器。之后,直接将Timer图标拖动到窗体上,即可为该窗体添加一个服务器计时器。
第二种方法 • 用编程方式创建 Timer组件的实例,代码如下。 1 Dim myTimer As New System.Timers.Timer( ) 2 myTimer.Interval = 3000 3 myTimer.Enabled = True
示例:窗体渐变退出 • 界面设计:
在窗体加载的时候初始化计时器。 下面代码是窗体的Load事件过程,完成计时器的初始化。 1 Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 2 ' 初始化计时器 3 Timer1.Interval = 100 4 Timer1.AutoReset = True 5 Timer1.Enabled = False 6 End Sub
单击【渐变退出】按钮后,启动计时器。 下面代码是【渐变退出】按钮的Click事件过程,启动计时器。 1 Private Sub btnClose_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnClose.Click 2 btnClose.Enabled = False ' 将窗体关闭按钮设置为不可用,防治在渐变过程中用户再次单击【关闭】按钮。 3 Timer1.Start() ' 启动TIMER开始计时 4 End Sub 5 ' 计时器启动后,每个100毫秒便触发Elapsed事件,该事件的处理过程如下 6 Private Sub Timer1_Elapsed(ByVal sender As System.Object, ByVal e As System.Timers.ElapsedEventArgs) Handles Timer1.Elapsed 7 Me.Opacity -= 0.07 ' 根据timer的计时频率(?毫秒)来不断使窗体透明度减少。 8 If Me.Opacity <= 0.15 Then 9 Timer1.Enabled = False ' 当窗体透明度小于15%时停止计时,既停止timer所在线程。 10 Me.Close() ' 销毁窗体资源 11 End If 12 End Sub
运行结果 • 窗体逐渐变得透明