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属性,然而,抛出的是一个空的字符串。。。。

这个例子只是说明不同的线程可以共享一个堆。。关于死锁的例子,就不说了。哈哈。

posted @ 2009-08-02 00:03  liuyan  阅读(841)  评论(0编辑  收藏  举报