一切福田,不離方寸,從心而覓,感無不通。

WinForm UI 多线程 (线程间操作无效)

一个简单的Form, 按钮btnTest是enabled=false。在btnEnable的Click事件中 创建线程,在线程中尝试设置btnTest.Enabled = true; 发生异常:线程间操作无效: 从不是创建控件“btnTest”的线程访问它。

代码如下:

复制代码

复制代码

在默认情况下,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; }

方法二:

复制代码

复制代码

方法三: 通过ISynchronizeInvoke和MethodInvoker.

复制代码

复制代码

MyEnableButton方法也可以如下:

复制代码

复制代码

对于方法三,可用于form中调用其他form的方法,具体解释及用法如下:

每一个从Control类中派生出来的WinForm类(包括Control类)都是依靠底层Windows消息和一个消息泵循环(message pump loop)来执行的。消息循环必须有一个对应的线程,因为发送window的消息实际上消息只会被发送到创建该window的线程中去。其结果是,即使提供了同步(synchronization),也无法从多线程中调用这些处理消息的方法。
大多数plumbing是掩藏起来的,因为WinForm是用代理(delegate)将消息绑定到事件处理方法中的。WinForm将Windows消息转换为一个基于代理的事件,但是必须注意,由于最初消息循环的缘故,只有创建该form的线程才能调用其事件处理方法。如果其他线程中调用这些方法,则它们会在该线程中处理事件,而不是在指定的线程中进行处理。

Control类(及其派生类)实现了一个定义在System.ComponentModel命名空间下的接口(ISynchronizeInvoke),并以此来处理多线程中调用消息处理方法的问题:

复制代码

复制代码

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方法:

复制代码