多线程使应用程序具有在同一时刻处理多个事务的能力。使用多线程,可以一个线程运行用户界面,同时另一个线程在后台做复杂的计算或处理。Microsoft Visual Basic .NET支持多线程,因此,我们可以轻易的实现这种能力。
不幸的是,多线程也有它不利的一面。任何时候某个应用程序使用的线程多于一个时,如果多个线程在同一时刻试图使用相同的数据或资源,可能出现麻烦。这种情况一旦出现,程序将变得非常复杂并且难以调试。
更糟的是多线程代码经常在最初开发时运行良好,在形成产品时却往往失败,原因在于有未被发现的多个线程与相同的数据或资源相互作用的情况。这使得多线程编程非常危险。
由于设计和调试多线程应用程序的困难性,微软提出了单线程单元(STA)的概念。Visual Basic 6代码通常在STA中运行,因此代码只需要考虑一个线程。这完全的避免了共享数据和资源的问题,但也意味着我们如果不使用其它技术就无法实现多线程。
在.NET中没有类似STA的东西。所有的.NET代码在AppDomain中运行,而AppDomain允许使用多线程。这意味着Visual Basic .NET代码也在AppDomain中运行,因此我们从多线程中受益。很明显,任何时候我们设计多线程应用程序,都必须小心编写代码以避免线程之间的冲突。
最简单的避免线程冲突的的方法是使线程之间永远不与相同的数据或资源交互。但这不一定可行,对任何多线程程序来说,避免或最小化共享数据或资源应作为一个目标。
这不但简化了编码和调试,而且增强了性能。为了解决线程间冲突的问题,我们必须使用同步技术,但同步技术常常引起某个线程阻塞或临时停顿,直到另一个线程完成某个动作。阻塞一个线程意味着让它空闲着,没有工作,降低了性能。
“取消”按钮和状态显示
在应用程序中使用多线程的原因很多,最普通的原因是要执行一个需要长时间运行的事务,并且希望用户界面与使用者保持响应。
至少,我们通常用一个“取消”按钮来响应用户,这样用户才能表示他们想中断某个事务。
在Visual Basic 6中,我们使用DoEvents和计时器控件和其它工作区的宿主来实现这种功能。在Visual Basic .NET中,这非常简单,因为我们可以使用多线程,只要小心,我们实现该功能根本不会增加代码和调试的复杂性。
在多线程环境中成功实现“取消”按钮的关键在于要记得该按钮仅仅请求事务被终止。是后台事务自己在适当的时候停止。
如果我们实现的“取消”按钮直接停止后台处理,可能会在一些敏感操作的中间停止了它,或在它关闭某些类似文件句柄或数据库连接等重要的资源前停止了它。这可能是致命的,会造成死锁,应用程序行为将变得不稳定或直接中断。
作为该方法的代替,“取消”按钮应该是请求后台事务停止。后台事务能在适当时候检查是否有“取消”请求。当后台线程检测到“取消”请求,便释放所有资源,停止临界活动并温和地终止。
尽管查询“取消”操作很重要,但是我们更希望用户界面(UI)能给使用者显示后台处理的状态信息。这种显示可以是文本信息或完成百分比,或两者都有。
在Visual Basic .NET实现“取消”按钮或状态显示所面临的复杂因素在于Windows Forms库不是线程安全(thread-safe)的。这意味着只有创建窗体的线程能访问该窗体和窗体的控件。其它线程不能安全的与该窗体或它的控件交互。
这意味着我们写代码时必须非常小心,保证只有我们的UI线程与该UI交互。为了确信这一点,我们可以建立一个简单的框架来管理后台工作线程与该UI线程的交互。如果我们做对了,结果便是我们使用的线程与UI代码和长时间运行事务的代码明显相关。
线程和对象
创建在自己的线程上使用自己的数据运行的后台处理的最简单的方法是为该后台处理建立一个对象。虽然这不一定可行,但却是一个好的目标,因为它从根本上简化了多线程应用程序。
如果后台线程在自己的对象中运行,它可以使用该对象的实例变量(在类中声明的变量),而不需担心它们被其它的线程使用了。例如,考虑下面的类:
Public Class Worker 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
Public Sub Work() Dim innerIndex As Integer Dim outerIndex As Integer Dim value As Double For outerIndex = 0 To mOuter For innerIndex = 0 To mInner ' do some cool calculation here value = Math.Sqrt(CDbl(innerIndex - outerIndex)) Next Next End Sub End Class |
该类设计用于运行后台线程,能使用下面的代码来执行它:
Dim myWorker As New Worker(10000000, 10) Dim backThread As New Thread(AddressOf myWorker.Work) backThread.Start() |
Worker类有用于存放数据的实例变量。这些变量(mInner和mOuter),可以被后台线程安全地使用,并可以断言它们不会在同一时刻被其它线程访问。
我们可以使用constructor方法来初始化该对象。在后台线程被载入前,主应用程序代码建立该对象的一个实例,并用后台线程操作的数据来初始化。
将该对象的Work方法的地址给后台线程后,它才被载入。该线程将使用对象中给定的数据运行的代码。
由于该对象是自我包含(self-contained)的,我们能建立多个对象,每一个在它们自己的线程上运行并且彼此隔离。
这种实现并不理想。UI没有办法知道后台处理的状态。我们也没有执行任何机制使得UI可以查询到后台处理已经终止了。
所有这些情况都需要后台线程以某种方式与UI线程交互。这种交互很复杂,因此如果我们能用某种方式抽象化这种交互,将它们放入一个类中将会很有利,这样UI和工作代码都不需要担心细节。
体系结构
我们可以建立一个体系结构来保护UI和工作代码,防止它们进行线程间交互。事实上,我们可以实现一个构架来执行那些复杂代码,我们能使用该构架来管理或控制后台线程与UI的交互。
首先让我们讨论该体系结构,然后再设计和实现该代码。
典型的情况下,一个应用程序以一个线程开始,该线程打开用户界面。为了直观我们称该线程为UI线程。在很多应用程序中这是唯一的线程,因此它控制UI并做所有的处理。
在本例中,我们将建立一个工作线程从事后台处理,让UI线程聚焦于用户界面,这样工作线程在忙于工作时,UI线程仍然可以响应用户。
在UI线程与工作线程之间我们插入一层代码作为UI和工作代码间的&&接口。该代码本质上是控制器(Controller),管理和控制工作线程与UI线程间的交互。

图1:UI线程,控制器和工作线程
控制器将包含所有这些代码:安全地开始工作线程,将工作线程的任何状态信息传递给UI线程,将任何“取消”请求从UI线程传递给工作线程。UI代码与工作代码并不直接交互;它们通过控制器代码交互。