在多线程中如何调用Winform

问题的产生

我的WinForm程序中有一个用于更新主窗口的工作线程(worker thread),但文档中却提示我不能在多线程中调用这个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;}
}

ISynchronizeInvoke提供了一个普通的标准机制用于在其他线程的对象中进行方法调用。例如,如果一个对象实现了ISynchronizeInvoke,那么在线程T1上的客户端可以在该对象中调用ISynchronizeInvoke的Invoke()方法。Invoke()方法的实现会阻塞(block)该线程的调用,它将调用打包发送(marshal)到 T2,并在T2中执行调用,再将返回值发送会T1,然后返回到T1的客户端。Invoke()方法以一个代理来定位该方法在T2中的调用,并以一个普通的对象数组做为其参数。

调用者还可以检查InvokeRequired属性,因为你既可以在同一线程中调用ISynchronizeInvoke也可以将它重新定位(redirect)到其他线程中去。如果InvokeRequired的返回值是false的话,则调用者可以直接调用该对象的方法。

比如,假设你想要从另一个线程中调用某个form中的Close方法,那么你可以使用预先定义好的的MethodInvoker代理,并调用Invoke方法:

Form form;
/* obtain a reference to the form, 
then: */
ISynchronizeInvoke synchronizer;
synchronizer = form;

if(synchronizer.InvokeRequired)
{
    MethodInvoker invoker = new MethodInvoker(form.Close);
    synchronizer.Invoke(invoker,null);
}
else
    form.Close();

ISynchronizeInvoke不仅仅用于WinForm中。例如,一个Calculator类提供了将两个数字相加的Add()方法,它就是通过ISynchronizeInvoke来实现的。用户必须确定ISynchronizeInvoke.Invoke()方法的调用是执行在正确的线程中的。

C# 在正确的线程中写入调用

列表A. Calculator类的Add()方法用于将两个数字相加。如果用户直接调用Add()方法,它会在该用户的线程中执行调用,而用户可以通过 ISynchronizeInvoke.Invoke()将调用写入正确的线程中。

列表A:

public class Calculator : ISynchronizeInvoke
{
    public int Add(int arg1, int arg2)
    {
        int threadID = Thread.CurrentThread.GetHashCode();
        Trace.WriteLine("Calculator thread ID is " + threadID.ToString());
        return arg1 + arg2;
    }
    //ISynchronizeInvoke implementation
    public object Invoke(Delegate method, object[] args)
    {
        public IAsyncResult BeginInvoke(Delegate method, object[] args)
        {
            public object EndInvoke(IAsyncResult result)
            {
                public bool InvokeRequired
                {
                }
            }

            //Client-side code
            public delegate int AddDelegate(int arg1, int arg2);

            int threadID = Thread.CurrentThread.GetHashCode();
            Trace.WriteLine("Client thread ID is " + threadID.ToString());

            Calculator calc;
            /* Some code to initialize calc */
            AddDelegate addDelegate = new AddDelegate(calc.Add);

            object[] arr = new object[2];
            arr[0] = 3;
            arr[1] = 4;

            int sum = 0;
            sum = (int)calc.Invoke(addDelegate, arr);
            Debug.Assert(sum == 7);

            /* Possible output:
            Calculator thread ID is 29
            Client thread ID is 30 
            */

或许你并不想进行同步调用,因为它被打包发送到另一个线程中去了。你可以通过BeginInvoke()EndInvoke()方法来实现它。你可以依照通用的.NET非同步编程模式(asynchronous programming model)来使用这些方法:用BeginInvoke()来发送调用,用EndInvoke()来实现等待或用于在完成时进行提示以及收集返回结果。

还值得一提的是ISynchronizeInvoke方法并非安全类型。 类型不符会导致在执行时被抛出异常,而不是编译错误。所以在使用ISynchronizeInvoke时要格外注意,因为编辑器无法检查出执行错误。

实现ISynchronizeInvoke要求你使用一个代理来在后期绑定(late binding)中动态地调用方法。每一种代理类型均提供DynamicInvoke()方法: public object DynamicInvoke(object[] args);

理论上来说,你必须将一个方法代理放到一个需要提供对象运行的真实的线程中去,并使Invoke()BeginInvoke()方法中的代理中调用DynamicInvoke()方法。ISynchronizeInvoke的实现是一个非同一般的编程技巧,本文附带的源文件中包含了一个名为Synchronizer的帮助类(helper class)和一个测试程序,这个测试程序是用来论证列表A中的Calculator类是如何用Synchronizer类来实现ISynchronizeInvoke的。SynchronizerISynchronizeInvoke的一个普通实现,你可以使用它的派生类或者将其本身作为一个对象来使用,并将ISynchronizeInvoke实现指派给它。

用来实现Synchronizer的一个重要元素是使用一个名为WorkerThread的嵌套类(nested class)。WorkerThread中有一个工作项目(work item)查询。WorkItem类中包含方法代理和参数。Invoke()和BeginInvoke()用来将一个工作项目实例加入到查询里。WorkerThread新建一个.NET worker线程,它负责监测工作项目的查询任务。查询到项目之后,worker会读取它们,然后调用DynamicInvoke()方法。

Contributors: FHL