VB .NET多线程编程的详细说明
作者:陶刚 整理:http://tupan.net 更新时间:2011-4-1
介绍
传统的Visual Basic开发人员已经建立了同步应用程序,在这些程序中事务按顺序执行。尽管由于多个事务多多少少地同时运行使多线程应用程序效率更高,但是使用先前版本的Visual Basic很难建立这类程序。
多线程程序是可行的,因为操作系统是多任务的,它有模拟同一时刻运行多个应用程序的能力。尽管多数个人计算机只有一个处理器,但是现在的操作系统还是通过在多个执行代码片断之间划分处理器时间提供了多任务。线程可能是整个应用程序,但通常是应用程序可以单独运行的一个部分。操作系统根据线程的优先级和离最近运行的时间长短给每一个线程分配处理时间。多线程对于时间密集型事务(例如文件输入输出)应用程序的性能有很大的提高。
但是也有必须细心的地方。尽管多线程能提高性能,但是每个线程还是需要用附加的内存来建立和处理器时间来运行,建立太多的线程可能降低应用程序的性能。当设计多线程应用程序时,应该比较性能与开销。
多任务成为操作系统的一部分已经很久了。但是直到最近Visual Basic程序员才能使用无文档记录特性(undocumented)或者间接使用COM组件或者操作系统的异步部分执行多线程事务。.NET框架组件为开发多线程应用程序,在System.Threading名字空间中提供了全面的支持。
本文讨论多线程的好处以及怎样使用Visual Basic .NET开发多线程应用程序。尽管Visual Basic .NET和.NET框架组件使开发多线程应用程序更容易,但是本文作了调整使其适合高级读者和希望从早期Visual Basic转移到Visual Basic .NET的开发人员。
多线程处理的优点
尽管同步应用程序易于开发,但是它们的性能通常比多线程应用程序低,因为一个新的事务必须等待前面的事务完成后才能开始。如果完成某个同步事务的时间比预想的要长,应用程序可能没有响应。多线程处理可以同时运行多个过程。例如,字处理程序能够在继续操作文档的同时执行拼写检查事务。因为多线程应用程序把程序分解为独立的事务,它们能通过下面的途径充分提高性能:
l 多线程技术可以使程序更容易响应,因为在其它工作继续时用户界面可以保持激活。 l 当前不忙的事务可以把处理器时间让给其它事务。
l 花费大量处理时间的事务可以周期性的把时间让给其它的事务。 l 事务可以在任何时候停止。
l 可以通过把单独事务的优先级调高或调低来优化性能。
明确地建立多线程应用程序的决定依赖于几个因素。多线程最适合下面的情况: l 时间密集或处理密集的事务妨碍用户界面。
l 单独的事务必须等待外部资源,例如远程文件或Internet连接。
例如,某个应用程序跟随Web页面上的链接并下载符合特定条件的文件。这种应用程序可以同步一个接一个地下载文件或者使用多线程在同一时刻下载多个文件。多线程的方法比同步方法的效率高得多,因为即使某些线程从远程Web服务器上接收到的响应很慢,文件也可以被下载。
建立新线程
建立线程的最直接的方法是建立线程类的一个新的实例并且使用AddressOf语句替你希望运行的过程传递一个委托。例如下面的代码运行一个作为单独的线程的叫做SomeTask的子过程。
Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask) Thread1.Start ' 这儿的代码立即运行 这就是建立和启动线程的全部工作。调用线程的Start方法后面的任何代码立即执行,不需要等待前面线程的结束。
下表是你能使用的控制单独线程的方法:
上面的大多数方法字面上容易理解,但是安全点(safe point)的概念对你来说可能是新的。安全点是代码中的某个位置,在这个位置通用语言运行时可以安全地执行自动无用单元收集(garbage collection,释放无用变量并恢复内存的过程)。当调用线程的Abort或Suspend方法时,通用语言运行时分析代码,决定线程停止运行的适当位置。
下表是线程的一些常用的属性:
当建立和管理线程时它的属性和方法很重要。本文的\线程同步\部分将讨论你怎样使用这些属性和方法控制和调整线程。
线程参数和返回值
前面例子中的线程调用没有参数和返回值。这是使用这种方法建立和运行线程的主要缺点之一。但是,你可以在类或结构体中包装线程,为运行在单独线程上的过程提供和返回参数。
Class TasksClass Friend StrArg As String Friend RetVal As Boolean Sub SomeTask() ' StrArg字段是一个参数 MsgBox(\ RetVal = True ' 设置返回参数中的返回值 End Sub End Class ' 为了使用这个类,设置存储参数的属性或者字段,接着异步调用需要的方法 Sub DoWork() Dim Tasks As New TasksClass() Dim Thread1 As New System.Threading.Thread(AddressOf Tasks.SomeTask) Tasks.StrArg = \设置作为参数使用的字段 Thread1.Start() ' 启动新线程 Thread1.Join() ' 等待线程1结束 ' 显示返回值 MsgBox(\ End Sub
手工建立和管理线程最适合于希望很好地控制细节(例如线程的优先级和线程模型)的应用程序。你可能想象,通过这种方法管理大量的线程是很困难的。在你需要很多线程时考虑使用线程池来减小复杂程度。
线程池
线程池是多线程的一种形式,在它里面,事务被添加到一个队列,并随着线程的建立自动启动。有了线程池,你使用希望运行的过程的委托调用Threadpool.QueueUserWorkItem方法,Visual Basic .NET就建立线程并运行该过程。下面的例子演示了怎样使用线程池启动几个事务:
Sub DoWork() Dim TPool As System.Threading.ThreadPool ' 对一个事务排队 TPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf SomeLongTask)) ' 对另一个事务排队 TPool.QueueUserWorkItem(New System.Threading.WaitCallback(AddressOf AnotherLongTask)) End Sub 当你需要启动很多单独事务而不需要单独设置每个线程的属性时,线程池是很有用的。每个线程使用默认的栈大小和优先级启动。默认情况下,每个系统处理器可以运行高达25个线程池线程。超过限制的线程可以排队,但是直到其它线程结束才能启动。
线程池的一个优点是你能把状态对象中的参数传递给每个事务过程。如果调用的过程需要一个以上参数,你可以把一个结构体或类的示例转换为Object数据类型。
参数和返回值
从线程池线程返回值有点棘手。从函数调用返回值的标准方法在这儿是不允许的,因为Sub过程是能被线程池排队的唯一过程类型。提供参数和返回值的途径是把这些参数,返回值和方法包装进一个包装类。提供参数和返回值的一个更简单的方法是使用QueueUserWorkItem方法的ByVal状态对象变量。如果使用该变量传递引用给类的一个实例,实例中的成员能被线程池线程修改并作为返回值使用。起先可以修改值传递的变量所引用的对象是不明显的,由于只有对象引用被值传递了,它才是可能的。当你修改对象引用引用的对象的成员时,改变应用到实际类的实例。
结构体不能用于在状态对象内部返回值。因为结构体是值类型的,异步处理做的改变不会改变原结构体的成员。当不需要返回值时使用结构体提供参数。
Friend Class StateObj Friend StrArg As String Friend IntArg As Integer Friend RetVal As String End Class Sub ThreadPoolTest() Dim TPool As System.Threading.ThreadPool Dim StObj1 As New StateObj() Dim StObj2 As New StateObj() ' 设置状态对象中的作为参数的一些字段 StObj1.IntArg = 10 StObj1.StrArg = \ StObj2.IntArg = 100 StObj2.StrArg = \ ' 对一个事务进行排队 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf SomeOtherTask), StObj1) ' 对另一个事务进行排队 TPool.QueueUserWorkItem(New System.Threading.WaitCallback _ (AddressOf AnotherTask), StObj2) End Sub Sub SomeOtherTask(ByVal StateObj As Object) ' 使用状态对象字段作为参数 Dim StObj As StateObj StObj = CType(StateObj, StateObj) ' 转换成正确的类型 MsgBox(\ MsgBox(\ ' 使用一个字段作为返回值 StObj.RetVal = \End Sub Sub AnotherTask(ByVal StateObj As Object) ' 使用状态对象作为参数。状态对象作为Object传递。把它转换为特定类型使使用更容易 Dim StObj As StateObj StObj = CType(StateObj, StateObj) MsgBox(\ MsgBox(\ ' 使用一个字段作为返回值 StObj.RetVal = \End Sub 通用语言运行时自动为排队的线程池事务建立线程,当这些事务完成时释放这些资源。一旦事务被排队了,这就不是取消事务的容易的方法了。ThreadPool线程使用多线程单元(MTA)线程模型运行。如果你希望线程使用单线程单元模型(STA)运行,必须手工建立线程。
线程同步