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();
}

posted on 2011-09-17 22:34  PeterZhang  阅读(25002)  评论(19编辑  收藏  举报