使用 Visual Basic .NET 进行多线程编程(转自MSDN)
发表于:2007-06-30来源:作者:点击数:
标签:
使用 Visual Basic .NET 进行多线程编程 Robert Burns Visual Studio Team Microsoft Corporation 2002 年 2 月 摘要: .NET 框架提供了新的类,可以方便地创建多线程应用程序。本文介绍如何使用 Visual Basic .NET 的多线程编程技术来 开发 效率更高、响应速
使用 Visual Basic .NET 进行多线程编程
Robert Burns
Visual Studio Team
Microsoft Corporation
2002 年 2 月
摘要:.NET 框架提供了新的类,可以方便地创建多线程应用程序。本文介绍如何使用 Visual Basic® .NET 的多线程编程技术来
开发效率更高、响应速度更快的应用程序。
目录
简介
过去,Visual Basic 开发人员创建的应用程序都是程序任务依次执行的同步应用程序。虽然多线程应用程序因多个任务几乎同时运行而具有更高的效率,但使用早期版本的 Visual Basic 来创建这样的应用程序却很困难。
一项称为多任务处理的操作系统功能使多线程程序成为可能,它能模拟同时运行多个应用程序的功能。虽然多数个人计算机都只安装了一个处理器,但现代操作系统通过将处理器时间分配给多段可执行代码(称为线程),提供了多任务处理功能。线程可以代表整个应用程序,但通常只代表应用程序中可单独运行的一部分。操作系统根据线程的优先级、上次运行线程后经过的时间等因素为每个线程分配处理时间。在执行耗时的任务(如文件输入和输出)时,多线程能够显著提高
性能。
但要注意一个问题。虽然多线程可以提高性能,但每个线程都需要额外的内存来创建线程,还需要处理器时间来运行线程。如果创建的线程过多,反而会降低应用程序的性能。在设计多线程应用程序时,应在添加更多线程所获得的好处及其成本之间进行权衡。
多任务处理成为操作系统的一部分已经很长时间了。但直到最近,Visual Basic
程序员也只能通过非正式发布的功能,来执行多线程任务,或者通过使用 COM 组件或操作系统的异步组件,来间接实现此功能。而 .NET 框架在
System.Threading 命名空间中为开发多线程应用程序提供了全面的支持。
本文讨论多线程的一些优点以及如何使用 Visual Basic .NET 来开发多线程应用程序。虽然 Visual Basic .NET 和 .NET 框架使多线程应用程序的开发变得很简单,但本文主要面向中高级开发人员,以及正在从 Visual Basic 的早期版本过渡到 Visual Basic .NET 的开发人员。对于 Visual Basic .NET 的初学者,请首先阅读 (英文)中的相应主题。
本文并非是对多线程编程的全面讨论。要获得更多的信息,请参阅本文最后列出的其他资源。
多线程处理的优点
同步应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。如果完成同步任务所用的时间比预计时间长,应用程序可能会不响应。多线程处理可以同时运行多个过程。例如,文字处理器应用程序在您处理文档的同时,可以检查拼写(作为单独的任务)。由于多线程应用程序将程序划分成独立的任务,因此可以在以下方面显著提高性能:
- 多线程技术使程序的响应速度更快,因为用户界面可以在进行其他工作的同时一直处于活动状态。
- 当前没有进行处理的任务可以将处理器时间让给其他任务。
- 占用大量处理时间的任务可以定期将处理器时间让给其他任务。
- 可以随时停止任务。
- 可以分别设置各个任务的优先级以优化性能。
是否需要创建多线程应用程序取决于多个因素。在以下情况下,最适合采用多线程处理:
- 耗时或大量占用处理器的任务阻塞用户界面操作。
- 各个任务必须等待外部资源(如远程文件或 Internet 连接)。
例如,用于跟踪 Web 页上的链接并
下载满足特定条件的文件的 Internet 应用程序“
robot”。这种应用程序可以依次同步下载各个文件,也可以使用多线程同时下载多个文件。多线程方法比同步方法的效率高很多,因为即使在某些线程中远程 Web
服务器的响应非常慢,也可以下载文件。
创建新线程
创建线程最直接的方法是创建新的线程类实例,并使用
AddressOf 语句为要运行的过程传递委托。例如,以下代码将名为 SomeTask 的子过程作为单独的线程运行。Dim Thread1 As New System.Threading.Thread(AddressOf SomeTask)Thread1.Start@# 此处的代码立即运行。
以上所述就是创建和启动线程的方法。在线程
Start 方法调用之后的任何代码将立即运行,而无需等待前一个线程运行结束。
下表列出了用于控制各个线程的一些方法。
方法操作
Start | 使线程开始运行。 |
Sleep | 使线程暂停一段指定的时间。 |
Suspend | 使线程在到达安全点后暂停。 |
Abort | 使线程在到达安全点后停止。 |
Resume | 重新启动挂起的线程。 |
Join | 使当前线程等待其他线程运行结束。如果使用超时值,且线程在分配的时间内结束,此方法将返回 True。 |
多数方法都无需再加以说明,但“安全点”可能是个新的概念。安全点是指代码中的某些位置,在这些位置公共语言运行时可以安全地执行自动垃圾回收,即释放未使用的变量并回收内存。调用线程的
Abort 或
Suspend 方法时,公共语言运行时将分析代码并确定线程停止运行的适当位置。
线程还包含许多有用的属性,如下表所示:
属性值
IsAlive | 如果线程处于活动状态,则包含值 True。 |
IsBackground | 获取或设置布尔值,指示线程是否是后台线程或是否应该是后台线程。后台线程与前台线程类似,但后台线程并不阻止进程的终止。当进程的所有前台线程都终止后,公共语言运行时将对仍处于活动状态的后台线程调用 Abort 方法,以结束该进程。 |
Name | 获取或设置线程的名称。常用于在调试时查找各个线程。 |
Priority | 获取或设置操作系统用来确定线程优先级安排的值。 |
ApartmentState | 获取或设置用于特定线程的线程模型。当线程调用非托管的代码时,线程模型将非常重要。 |
ThreadState | 包含说明线程状态的值。 |
线程属性和方法对创建和管理线程非常有用。本文的线程同步部分将介绍如何使用这些属性和方法控制和协调线程。
线程参数和返回值
前面示例中的方法调用不能包含任何参数或返回值。这一限制是使用此方法创建和运行线程的主要缺点之一。然而,可以通过将在单独的线程中运行的过程包装到类或结构中,为它们提供参数,并使之能返回参数。Class TasksClass Friend StrArg As String Friend RetVal As Boolean Sub SomeTask() @# 将 StrArg 字段用作参数。 MsgBox("StrArg 包含字符串" & StrArg) RetVal = True @# 设置返回参数的返回值。 End SubEnd 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("线程 1 返回值" & Tasks.RetVal)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 StringEnd ClassSub 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 SubSub SomeOtherTask(ByVal StateObj As Object) @# 将状态对象字段用作参数。 Dim StObj As StateObj StObj = CType(StateObj, StateObj) @# 强制转换为正确的类型。 MsgBox("StrArg 包含字符串" & StObj.StrArg) MsgBox("IntArg 包含数字" & CStr(StObj.IntArg)) @# 将字段用作返回值。 StObj.RetVal = "SomeOtherTask 的返回值"End SubSub AnotherTask(ByVal StateObj As Object) @# 将状态对象字段用作参数。 @# 状态对象作为 Object 进行传递。 @# 将其强制转换为特定的类型以使其更易于使用。 Dim StObj As StateObj StObj = CType(StateObj, StateObj) MsgBox("StrArg 包含字符串 " & StObj.StrArg) MsgBox("IntArg 包含数字" & CStr(StObj.IntArg)) @# 将字段用作返回值。 StObj.RetVal = "AnotherTask 的返回值"End Sub
公共语言运行时自动为排队的线程池任务创建线程,然后,当任务完成后释放这些资源。将任务排队后,很难再将其取消。
ThreadPool 线程始终使用多线程单元 (MTA) 线程模型来运行。如果需要使用单线程单元 (STA) 模型的线程,则应手动创建线程。
原文转自:http://www.ltesting.net