默认情况下,C#不允许在一个线程中直接操作另一个线程中的控件,这是因为访问Windows窗体控件本质上不是线程安全的。如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。还可能出现其他与线程相关的bug,以及不同线程争用控件引起的死锁问题。因此确保以线程安全方式访问控件非常重要。
在调试器中运行应用程序时,如果创建某控件的线程之外的其他线程试图调用该控件,则调试器会引发一个InvalidOperationException异常,并提示消息:“从不是创建控件的线程访问它”。
但是在Windows应用程序中,为了在窗体上显示线程中处理的信息,我们可能需要经常在一个线程中引用另一个线程中的窗体控件。比较常用的办法之一是使用委托(delegate)来完成这个工作。
为了区别是否是创建控件的线程访问该控件对象,Windows应用程序中的每一个控件对象都有一个InvokeRequired属性,用于检查是否需要通过调用Invoke方法完成其他线程对该控件的操作,如果该属性为true,说明是其他线程操作该控件,这时可以创建一个委托实例,然后调用控件对象的Invoke方法,并传入需要的参数完成相应操作,否则可以直接对该控件对象进行操作,从而保证了安全代码下线程间的互操作。例如:
delegate void AppendStringDelegate(string str);
private void AppendString(string str)
{
if (richTextBox1.InvokeRequired)
{
AppendStringDelegate d = new AppendStringDelegate(AppendString);
richTextBox1.Invoke(d, "abc");
}
else
{
richTextBox1.Text += str;
}
}
这段代码中,首先判断是否需要通过委托调用对richTextBox1的操作,如果需要,则创建一个委托实例,并传入需要的参数完成else代码块的功能;否则直接执行else代码块中的内容。
实际上,由于我们在编写程序时就已经知道控件是在哪个线程中创建的,因此也可以在不是创建控件的线程中直接调用控件对象的Invoke方法完成对该线程中的控件的操作。
注意,不论是否判断InvokeRequired属性,委托中参数的个数和类型必须与传递给委托的方法需要的参数个数和类型完全相同。
【例1-3】一个线程操作另一个线程的控件的方法。
(1) 新建一个名为ThreadControlExample的Windows应用程序,界面设计如图1-4所示。
图1-4 例1-3的设计界面 |
图1-5 例1-3的运行界面 |
(2) 添加命名空间引用:
using System.Threading;
(3) 在构造函数上方添加字段声明,并在构造函数中初始化对象:
Thread thread1;
Thread thread2;
delegate void AppendStringDelegate(string str);
AppendStringDelegate appendStringDelegate;
public Form1()
{
InitializeComponent();
appendStringDelegate = new AppendStringDelegate(AppendString);
}
(4) 直接添加代码:
private void AppendString(string str)
{
richTextBox1.Text += str;
}
private void Method1()
{
while (true)
{
Thread.Sleep(100); //线程1休眠100毫秒
richTextBox1.Invoke(appendStringDelegate, "a");
}
}
private void Method2()
{
while (true)
{
Thread.Sleep(100); //线程2休眠100毫秒
richTextBox1.Invoke(appendStringDelegate, "b");
}
}
(5) 分别在【启动线程】和【终止线程】按钮的Click事件中添加代码:
private void buttonStart_Click(object sender, EventArgs e)
{
richTextBox1.Text = "";
thread1 = new Thread(new ThreadStart(Method1));
thread2 = new Thread(new ThreadStart(Method2));
thread1.Start();
thread2.Start();
}
private void buttonStop_Click(object sender, EventArgs e)
{
thread1.Abort();
thread1.Join();
thread2.Abort();
thread2.Join();
MessageBox.Show("线程1、2终止成功");
}
(6) 按<F5>键编译并执行,单击【启动线程】后,再单击【终止线程】,运行结果如图1-5所示。