在一个Windows窗体应用程序中使用多线程,它具有实际的意义,同时尽量使事情简单
作者:Jason Clark 来源:论坛 2007年11月13日
关键字:
细节-实现一个取消按钮
有时你想为你的用户提供一种取消长操作的方法。你所需要的就是你的主线程同后台线程之间的一些通信方法,通知后台线程操作不再被需要,可以停止。System.Threading名字空间为这个方法提供了一个类:AutoResetEvent。
AutoResetEvent是线程间通信的一种简单机制。一个AutoResetEvent对象可以有两种状态中的一个:有信号的和无信号的。当你创建一个AutoResetEvent实例时,你可以通过构造函数的参数来决定其初始状态。然后感知该对象的线程通过检查AutoResetEvent对象的状态,或者用 AutoResetEvent对象的Set或Reset方法调整其状态,进行相互通信。
在某种程度上AutoResetEvent很像一个布尔类型,但是它提供的特征使其更适合于在线程间进行通信。这样的一个例子就是它有这种能力:一个线程可以有效的等待直到一个AutoResetEvent对象从一个无信号的状态变为有信号的状态。它是通过在该对象上调用WaitOne实现的。任何一个线程对一个无信号的AutoResetEvent对象调用了WaitOne,就会被有效的阻塞直到其它线程使该对象有信号。使用布尔变量线程必须在一个循环中登记该变量,这是无效率的。一般来说没有必要使用Reset来使一个AutoResetEvent变为无信号,因为当其它线程感知到该对象为有信号时,它会被立即自动的设为无信号的。
现在你需要一种让你的后台线程无阻塞的测试AutoResetEvent对象的方法,你会有许多工具实现线程的取消。为了完成这些,调用带有WaitOne的重载窗体并指出一个零毫秒的超出时间,以零毫秒为超出时间的WaitOne会立即返回,而不管AutoResetEvent对象的状态是否为有信号。如果返回值为true,这个对象是有信号的;否则由于时间超出而返回。
我们整理一下实现取消的特点。如果你想实现一个取消按钮,它能够取消后台线程中的一个长操作,按照以下步骤:
在你的窗体上加入AutoResetEvent域类型
通过在AutoResetEvent的构造函数中传入false参数,设置该对象初始状态为无信号的。 接着在你的窗体上保 存该对象的引用域,这是为了能够在窗体的整个生命周期内可以对后台线程的后台操作实现取消操作。
在你窗体上加入一个取消按钮。
在取消按钮的Click事件处理器中,通过调用AutoResetEvent对象的Set方法使其有信号。
同时,在你的后台线程的逻辑中周期性地在AutoResetEvent对象上调用WaitOne来检查用户是否取消了。
if(cancelEvent.WaitOne(0, false)){
// cancel operation
}
你必须记住使用零毫秒参数,这样可以避免在后台线程操作中不必要的停顿。
如果用户取消了操作,通过主线程AutoResetEvent会被设为有信号的。 当WaitOne返回true时你的后台线程会 得到警告,并停止操作。同时在后台线程中由于调用了WaitOne该事件会被自动的置为无信号状态。
为了能够看到取消长操作窗体的例子,你可以下载CancelableForm.cs文件。这个代码是一个完整的程序,它与Figure 4 中的CorrectMultiThreadedForm.cs只有稍微的不同。
注意在CancelableForm.cs也采用了比较高级的用法Control.Invoke, 在那里EnableControls方法被设计用来调用它自己如果当它被一个错误的线程所调用时。在它使用窗体上的任何GUI对象的方法或属性时要先做这个检查。 这样能够使得EnableControls能够从任何线程中直接安全的调用,在方法的实现中有效的隐藏了Invoke调用的复杂性。这些可以使应用程序更加有维护性。注意在这个例子中同样使用了Control.BeginInvoke, 它是Control.Invoke的异步版本。
你也许注意到取消的逻辑依赖于后台线程通过WaitOne调用周期性的取消检查的能力。 但是如果正在讨论的问题不能被取消怎么办?如果后台操作是一个单个调用,像DataAdapter.Fill,它会花很长时间?有时会有解决办法的,但并不总是。
如果你的长操作根本不能取消,你可以使用一个伪取消的方法来完成你的操作,但在你的程序中不要影响你的操作结果。这不是技术上的取消操作,它把一个可忍受的操作帮定到一个线程池中,但这是在某种情况下的一种折中办法。如果你实现了类似的解决办法,你应该从你的取消按钮事件处理器中直接使能你已禁止的UI元素,而不要还依赖于被绑定的后台线程通过Invoke调用使能你的控件。同样重要的使设计你的后台操作线程,当其返回时测试一下它是否被取消,以便它不影响现在被取消的操作的结果。
这种长操作取消是比较高级的方法,它只在某些情况下才可行。例如,数据库查询的伪取消就是这样,但是一个数据库的更新,删除,插入伪取消是一个滞后的操作。有永久的操作结果或与反馈有关的操作,像声音和图像,就不容易使用伪取消方法,因为操作的结果在用户取消以后是非常明显的。