InvokeRequired and Invoke
在.net中,控件的访问更新只能在"拥有"这个控件的线程上执行,否则回抛异常。
MS在Control类上提供了一个InvokeRequired的属性。下面是MSDN对这个属性的一个注释。我这里只有中文版的。呜呜。
Control.InvokeRequired 属性获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。
我查过英文版的注释,无论是中文的还是英文的,都强调了一点,就是说控件只能在创建此控件的线程上才能被调用。但是实际上却不是这样子的。
正确地说,这里有两个概念,即创建控件的线程和拥有控件的线程。关于"拥有"这个词,这里只是我自己YY出来的,但愿能说明问题,后面还有更多的解释。
创建控件的线程很好理解,那什么是拥有控件的线程呢。两者是一样的吗?不一样。
拥有控件的线程。我这里指的应该是创建了此控件handle的线程,而不是创建控件本身的那个线程。两者可以是相同的,也可以是不同的。
那线程怎么样创建一个控件的handle呢?很简单,我们还是先看看MSDN对Handle的说明。
Control.Handle 属性
获取控件绑定到的窗口句柄。备注
Handle 属性的值是 Windows HWND。如果句柄尚未创建,引用该属性将强制创建句柄。
在实际中,当一个控件被创建出来后,它的Handle是还没有被创建的,只有当第一次引用了该发生之后,才会被创建。
下面是我写的一段代码。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Threading;
namespace InvokeRequiredDemo
{
public partial class Form1 : Form
{
Control controlCreatedFromOtherThread;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (null == controlCreatedFromOtherThread)
{
Thread.Sleep(100);
}
MessageBox.Show(controlCreatedFromOtherThread.InvokeRequired.ToString());
}
private void CreateControlInstance()
{
Control newControlInstance = new Control();
controlCreatedFromOtherThread = newControlInstance;
}
}
}
运行这段代码,对话框抛出的是false。说明主线程是可以调用更新控件的,虽然这个控件是在另一个线程中被创建。
下面我们改一下CreateControlInstance()这个方法。
private void CreateControlInstance()
{
Control newControlInstance = new Control();
IntPtr intPtr = newControlInstance.Handle;
controlCreatedFromOtherThread = newControlInstance;
}
我们在代码中加多了一名IntPtr intPtr = newControlInstance.Handle;这句代码将让创建控件的线程(不是主线程)去访问控件的Handle,代码更改后运行,对话框抛出的是true。
关于Control.Handle,还有另一个有趣的东东。比如上面的代码中,你让创建控件的线程同时创建它的Handle,再回到主线程中,这时候,你试着访问它的Handle,会发生什么呢?
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (null == controlCreatedFromOtherThread)
{
Thread.Sleep(100);
}
MessageBox.Show(controlCreatedFromOtherThread.Handle.ToString());
}
答案是抛异常。线程间操作无效: 从不是创建控件“”的线程访问它。
再改改代码:
public delegate void SampleDelegate();
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (null == controlCreatedFromOtherThread)
{
Thread.Sleep(100);
}
SampleDelegate sampleDelegate = new SampleDelegate(SampleMethod);
controlCreatedFromOtherThread.Invoke(
sampleDelegate);
}
public void SampleMethod()
{
// Nothing to do
// Please try to make a breakpoint here.
// In this case, no stack here when run the demo.
}
运行,你会发现,代码根本没有走到SampleMethod()里面去,很奇怪,我在公司时试的结果,是直接在controlCreatedFromOtherThread.Invoke上抛异常的(NullReference,但是我们知道,controlCreatedFromOtherThread并不是空的)。可能是framework或是IDE不一样。公司是VS2005?C#EXPRESS?家里是VS2008。
把controlCreatedFromOtherThread.Invoke改成this.Invoke,SampleMethod()就可以走进去了。
我猜测应该是在用Invoke时,是会需要根据Handle来找到拥有控件的线程,然后在这个线程里调用代理的方法,上面的代码因为主线程拿不到Handle,所以为null,故抛了异常。
最后,再来说说这个Handle是什么时候会被创建。MSDN上说被引用的时候会创建,那什么时候是会被引用呢?直接Control.Handle,当然是,除此之外,还有其它一些case。
比如把控件加到窗体上去,这个控件的Handle也会被窗体所在的线程所创建。事实上,如果控件和窗体的Handle不是在同一样线程上,你是无法把它加到窗体上去的。比如:
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (null == controlCreatedFromOtherThread)
{
Thread.Sleep(100);
}
this.Controls.Add(controlCreatedFromOtherThread);
}
我VS2008中,会抛出“线程间操作无效: 从不是创建控件“”的线程访问它。”的异常。在公司时抛出的异常更加详细,大概就是说控件不能被加到窗体中,因为他们的Handle不是在同一个纯种上。
再看下面的例子:
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (null == controlCreatedFromOtherThread)
{
Thread.Sleep(100);
}
this.Controls.Add(controlCreatedFromOtherThread);
}
private void CreateControlInstance()
{
Control newControlInstance = new Control();
Form newForm = new Form();
newForm.Controls.Add(newControlInstance);
controlCreatedFromOtherThread = newControlInstance;
}
注意这里在CreateControlInstance中又创建了一个窗体,并把新创建的控件加到窗体上面去,但是实际上,这个时候控件的Handle还是没有被创建出来,我们运行代码,是不会有异常的。之所以这个,是因为这个新的窗体还没有被Show出来。
再改改:
private void CreateControlInstance()
{
Control newControlInstance = new Control();
Form newForm = new Form();
newForm.Controls.Add(newControlInstance);
newForm.Show();
controlCreatedFromOtherThread = newControlInstance;
}
加了newForm.Show(),再运行,异常就出来了。说明新控件的Handle随着窗体的Show也一起被创建。Show方法应该是会先创建窗体的Handle,再创建里面子控件的Handle。
最后补充一点,其实在同一个进程中的不同线程是共享同一堆的,也就是可以共享一片内存。但是为什么不同的线程不能共享控件的Handle呢?我估计是MS特意这样子做,因为如果Handle被共享,在多线程的环境中很容易会出现死锁。
Control controlCreatedFromOtherThread;
IntPtr controlIntPtrFromOtherThread;
private void Form1_Load(object sender, EventArgs e)
{
Thread createControlThread = new Thread(
new ThreadStart(CreateControlInstance));
createControlThread.Start();
while (IntPtr.Zero.Equals(controlIntPtrFromOtherThread))
{
Thread.Sleep(100);
}
controlCreatedFromOtherThread = Control.FromHandle(controlIntPtrFromOtherThread);
MessageBox.Show(controlCreatedFromOtherThread.Name);
}
private void CreateControlInstance()
{
Control newControlInstance = new Control();
newControlInstance.Name = "hello, leland";
controlIntPtrFromOtherThread = newControlInstance.Handle;
}
这个例子中,在非主线程中创建了控件和Handle,并把Handle保存在全局变量中,回到主线程,还是可以根据Handle得到这个控件。
不过,很诡异的事情发生了。上面是把"hello,leland"给了控件的Name属性,在主线程的对话框中,抛出来的还是"hello,leland",this is correct. 我试着把"hello,leland"给Text属性,然而,抛出的是一个空的字符串。。。。
这个例子只是说明不同的线程可以共享一个堆。。关于死锁的例子,就不说了。哈哈。