WinForm UI 多线程 (线程间操作无效)
一个简单的Form, 按钮btnTest是enabled=false。在btnEnable的Click事件中 创建线程,在线程中尝试设置btnTest.Enabled = true; 发生异常:线程间操作无效: 从不是创建控件“btnTest”的线程访问它。
代码如下:
1 using System;
2 using System.Threading;
3 using System.Windows.Forms;
4
5 namespace TestingUIThread
6 {
7 public partial class Form1 : Form
8 {
9 Thread thread = null;
10
11 public Form1()
12 {
13 InitializeComponent();
14 }
15
16 private void btnEnable_Click(object sender, EventArgs e)
17 {
18 thread = new Thread(new ParameterizedThreadStart(EnableButton));
19 thread.Start(null);
20 }
21
22 private void EnableButton(object o)
23 {
24 // following line cause exception, details as below:
25 //Cross-thread operation not valid: Control 'btnTest' accessed from a thread other than the thread it was created on.
26 btnTest.Enabled = true;
27 btnTest.Text = "Enabled";
28 }
29 }
30 }
在默认情况下,C#不准许在一个线程中直接访问或操作另一线程中创建的控件,这是因为访问windows窗体控件本质上是不安全的。
线程之间是可以同时运行的,那么如果有两个或多个线程同时操作某一控件的某状态,尝试将一个控件变为自己需要的状态时, 线程的死锁就可能发生。
为了区别是否是创建该控件的线程访问该控件,Windows窗体控件中的每个控件都有一个InvokeRequired属性,这个属性就是用来检查本控件是否被其他线程调用的属性,当被创建该线程外的线程调用的时候InvokeRequired就为true。有了这个属性我们就可以利用它来做判断了。
光判断出是否被其他线程调用是没有用的,所以windows窗体控件中还有一个Invoke方法可以帮我们完成其他线程对控件的调用。结合代理的使用就可以很好的完成我们的目标。
上面问题的解决方法:
方法一:
public Form1()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
}
//
// Summary:
// Gets or sets a value indicating whether to catch calls on the wrong thread
// that access a control's System.Windows.Forms.Control.Handle property when
// an application is being debugged.
//
// Returns:
// true if calls on the wrong thread are caught; otherwise, false.
[EditorBrowsable(EditorBrowsableState.Advanced)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription("ControlCheckForIllegalCrossThreadCalls")]
[Browsable(false)]
public static bool CheckForIllegalCrossThreadCalls { get; set; }
方法二:
1 using System;
2 using System.Threading;
3 using System.Windows.Forms;
4
5 namespace TestingUIThread
6 {
7 public partial class Form1 : Form
8 {
9 Thread thread = null;
10
11 public Form1()
12 {
13
14 InitializeComponent();
15 }
16
17 private void btnEnable_Click(object sender, EventArgs e)
18 {
19 thread = new Thread(new ParameterizedThreadStart(EnableButton));
20 thread.Start(null);
21 }
22
23 private void EnableButton(object o)
24 {
25 EnableButton();
26 }
27
28 private delegate void EnableButtonCallBack();
29
30 private void EnableButton()
31 {
32 if (this.btnTest.InvokeRequired)
33 {
34 this.Invoke(new EnableButtonCallBack(EnableButton));
35 }
36 else
37 {
38 btnTest.Enabled = true;
39 btnTest.Text = "Enabled";
40 }
41 }
42 }
43 }
方法三: 通过ISynchronizeInvoke和MethodInvoker.
1 using System;
2 using System.ComponentModel;
3 using System.Threading;
4 using System.Windows.Forms;
5
6 namespace TestingUIThread
7 {
8 public partial class Form1 : Form
9 {
10 Thread thread = null;
11
12 public Form1()
13 {
14 InitializeComponent();
15 }
16
17 private void btnEnable_Click(object sender, EventArgs e)
18 {
19 thread = new Thread(new ParameterizedThreadStart(MyEnableButton));
20 thread.Start(null);
21 }
22
23 private void MyEnableButton(object sender)
24 {
25 ISynchronizeInvoke synchronizer = this;
26
27 if (synchronizer.InvokeRequired)
28 {
29 MethodInvoker minvoker = new MethodInvoker(this.EnableButton);
30 synchronizer.Invoke(minvoker, null);
31 }
32 else
33 {
34 this.EnableButton();
35 }
36 }
37
38 private void EnableButton()
39 {
40 btnTest.Enabled = true;
41 btnTest.Text = "Enabled";
42 }
43 }
44 }
MyEnableButton方法也可以如下:
private void MyEnableButton(object sender)
{
if (this.InvokeRequired)
{
MethodInvoker minvoker = new MethodInvoker(this.EnableButton);
this.Invoke(minvoker, null);
}
else
{
this.EnableButton();
}
}
对于方法三,可用于form中调用其他form的方法,具体解释及用法如下:
每一个从Control类中派生出来的WinForm类(包括Control类)都是依靠底层Windows消息和一个消息泵循环(message pump loop)来执行的。消息循环必须有一个对应的线程,因为发送window的消息实际上消息只会被发送到创建该window的线程中去。其结果是,即使提供了同步(synchronization),也无法从多线程中调用这些处理消息的方法。
大多数plumbing是掩藏起来的,因为WinForm是用代理(delegate)将消息绑定到事件处理方法中的。WinForm将Windows消息转换为一个基于代理的事件,但是必须注意,由于最初消息循环的缘故,只有创建该form的线程才能调用其事件处理方法。如果其他线程中调用这些方法,则它们会在该线程中处理事件,而不是在指定的线程中进行处理。
Control类(及其派生类)实现了一个定义在System.ComponentModel命名空间下的接口(ISynchronizeInvoke),并以此来处理多线程中调用消息处理方法的问题:
public interface ISynchronizeInvoke
{
object Invoke(Delegate method,object[] args);
IAsyncResult BeginInvoke(Delegate method,object[] args);
object EndInvoke(IAsyncResult result);
bool InvokeRequired {get;}
}
Invoke 同步调用
BeginInvoke和EndInvoke 异步调用,用BeginInvoke()来发送调用,用EndInvoke()来实现等待或用于在完成时进行提示以及收集返回结果。
ISynchronizeInvoke提供了一个普通的标准机制用于在其他线程的对象中进行方法调用。
例如,如果一个对象实现了ISynchronizeInvoke,那么在线程T1上的客户端可以在该对象中调用ISynchronizeInvoke的Invoke()方法。Invoke()方法的实现会阻塞(block)该线程的调用,它将调用打包发送(marshal)到 T2,并在T2中执行调用,再将返回值发送会T1,然后返回到T1的客户端。Invoke()方法以一个代理来定位该方法在T2中的调用,并以一个普通的对象数组做为其参数。
调用者可以检查InvokeRequired属性,因为既可以在同一线程中调用ISynchronizeInvoke也可以将它重新定位(redirect)到其他线程中去。如果InvokeRequired的返回值是false的话,则调用者可以直接调用该对象的方法。
比如,要从另一个线程中调用某个form中的method方法,那么可以使用预先定义好的的MethodInvoker代理,并调用Invoke方法:
ISynchronizeInvoke synchronizer = form;
if(synchronizer.InvokeRequired)
{
MethodInvoker invoker = new MethodInvoker(form.method);
synchronizer.Invoke(invoker,null);
}
else
{
form.method();
}