在一个Windows窗体应用程序中使用多线程,它具有实际的意义,同时尽量使事情简单
作者:Jason Clark 来源:论坛 2007年11月13日
关键字:
最简单的线程同步
在本栏目开始我就称保持线程同步而不互相冲突是一门艺术。Figure 3 所示的FlawedMultiThreadForm.cs应用程序有一个问题:用户可以通过单击按钮引发一个很长的响铃操作,他们可以继续单击按钮而引发更多的响铃操作。如果不是响铃,该长操作是数据库查询或者在进程的内存中进行数据结构操作,你一定不想在同一时间内,有一个以上的线程做同样的工作。最好的情况下这是系统资源的一种浪费,最坏的情况下会导致数据毁灭。
最容易的解决办法就是禁止按钮一类的用户交互元素;两个进程间的通信稍微有点难度。过一会我将给你看如何做这些事情。但首先,让我指出所有线程同步使用的一些线程间通信的形式-从一个线程到另一个线程通信的一种手段。稍后我将讨论大家所熟知的AutoResetEvent对象类型,它仅用在线程间通信。
现在让我们首先看一下为Figure 3 中FlawedMultiThreadedForm.cs程序中加入的线程同步代码。再一次的,Figure 4 CorrectMultiThreadedForm.cs程序中红色部分表示的是其先前程序的较小的改动部分。 如果你运行这个程序你将看到当一个长响铃操作在进行时用户交互被禁止了(但没有挂起),响铃完成的时候又被允许了。这次这些代码的变化已经足够了,我将逐个运行他们。
Figure 4 CorrectMultiThreadedForm.cs
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualBasic;
class App {
// Application entry point
public static void Main() {
// Run a Windows Forms message loop
Application.Run(new CorrectMultiThreadedForm());
}
}
// A Form-derived type
class CorrectMultiThreadedForm : Form{
// Constructor method
public CorrectMultiThreadedForm() {
// Create a textbox
text.Location = new Point(10, 10);
text.Size = new Size(50, 20);
Controls.Add(text);
// Create a button
button.Text = "Beep";
button.Size = new Size(50, 20);
button.Location = new Point(80, 10);
// Register Click event handler
button.Click += new EventHandler(OnClick);
Controls.Add(button);
// Cache a delegate for repeated reuse
enableControls = new BooleanCallback(EnableControls);
}
// Method called by the button's Click event
void OnClick(Object sender, EventArgs args) {
// Get an int from a string
Int32 count = 0;
try { count = Int32.Parse(text.Text); } catch (FormatException) {}
// Count to that number
EnableControls(false);
WaitCallback async = new WaitCallback(Count);
ThreadPool.QueueUserWorkItem(async, count);
}
// Async method beeps once per second
void Count(Object param) {
Int32 seconds = (Int32) param;
for (Int32 index = 0; index < seconds; index++) {
Interaction.Beep();
Thread.Sleep(1000);
}
Invoke(enableControls, new Object[]{true});
}
void EnableControls(Boolean enable) {
button.Enabled = enable;
text.Enabled = enable;
}
// A delegate type and matching field
delegate void BooleanCallback(Boolean enable);
BooleanCallback enableControls;
// Some private fields by which to reference controls
Button button = new Button();
TextBox text = new TextBox();
}
在Figure 4 的末尾处有一个EnableControls的新方法,它允许或禁止窗体上的文本框和按钮控件。在Figure 4 的开始我加入了一个EnableControls调用,在后台响铃操作排队等候之前立即禁止文本框和按钮。到这里线程的同步工作已经完成了一半,因为禁止了用户交互,所以用户不能引发更多的后台冲突操作。在Figure 4 的末尾你将看到一个名为BooleanCallback的委托类型被定义,其签名是同EnableControls方法兼容的。在那个定义之前,一个名为EnableControls的委托域被定义(见例子),它引用了该窗体的EnableControls方法。这个委托域在代码的开始处被分配。
你也将看到一个来自主线程的回调,该主线程为窗体和其控件拥有和提取消息。这个调用通过向EnableControls传递一个true参数来使能控件。这通过后台线程调用窗体的Invoke方法来完成,当其一旦完成其长响铃操时。代码传送的委托引用EnableControls去Invoke,该方法的参数带有一个对象数组。Invoke方法是线程间通信的一个非常灵活的方式,特别是对于Windows Forms类库中的窗口或窗体。在这个例子中,Invoke被用来告诉主GUI线程通过调用EnableControls方法重新使能窗体上的控件。
Figure 4 中的CorrectMultiThreadedForm.cs的变化实现了我早先的建议――当响铃操作在执行时你不想运行,就禁止引发响铃操作的用户交互部分。当操作完成时,告诉主线程重新使能被禁止的部分。对Invoke的调用是唯一的,这一点应该注意。
Invoke方法在 System.Windows.Forms.Controls类型中定义,包含Form类型让类库中的所有派生控件都可使用该方法。Invoke方法的目的是配置了一个从任何线程对为窗体或控件实现消息提取线程的调用。
当访问控件派生类时,包括Form类,从提取控件消息的线程来看你必须这样做。这在单线程的应用程序中是很自然的事情。但是当你从线程池中使用多线程时,要避免从后台线程中调用用户交互对象的方法和属性是很重要的。相反,你必须使用控件的Invoke方法间接的访问它们。Invoke是控件中很少见的一个可以安全的从任何线程中调用的方法,因为它是用Win32的PostMessage API实现的。
使用Control.Invoke方法进行线程间的通信有点复杂。但是一旦你熟悉了这个过程,你就有了在你的客户端程序中实现多线程目标的工具。本栏目的剩余部分将覆盖其它一些细节,但是Figure 4 中的CorrectMultiThreadedForm.cs应用程序是一个完整的解决办法:当执行任意长的操作时仍然能够响应用户的其它操作。尽管大多数的用户交互被禁止,但用户仍然可以重新配置和调整窗口,也可以关闭程序。然而,用户不能任意使用程序的异步行为。这个小细节能够让你对你的程序保持自信心。
在我的第一个线程同步程序中,没有使用任何传统的线程结构,例如互斥或信号量,似乎一钱不值。然而,我却使用了禁止控件的最普通的方法。