Invoke和BeginInvoke的详细理解(C#)
在项目中,当使用Winform( 基于.NET Framework)构建程序的GUI界面时,若以TextBox为列,要在其它线程中更新控件的界面显示,初学者往往是使用:
this.textBox.Text = "10";
当这样使用时,在程序员运行后可能会使程序触发“不能从不是创建该控件的线程中调用它”的异常信息。造成这种异常的原因在于,窗体控件是在主线程中创建的(比如this.Controls.Add(控件的组件对象)),当更新控件的界面显示时,是在其它线程,并不是在主线程中进行。当在其它线程中更新控件的显示界面时,可能会与主线程发生线程冲突。若此时主线程正在重绘此控件,而其它线程也在更新此控件的显示界面,其它线程就会和主线程进行竞争,造成画面混乱,甚至可能会出现死锁。
因此在Windows GUI编程中有一个规则,只能通过创建控件的线程来操作控件的数据,否则会产生不可预料的后果。
因此在.NET中,为了更好的解决这些问题,通过Control类实现了ISynchronizeInvoke接口,提供了Invoke和BeginInvoke方法,提供让其它线程对GUI界面的操作。ISynchronizeInvoke接口详细信息如下所示:
public interface ISynchronizeInvoke
[HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]
IAsyncResult BeginInvoke(Delegate method, object[] args);
object EndInvoke(IAsyncResult result);
object Invoke(Delegate method, object[] args);
bool InvokeRequired { get; }
}
如果从创建控件的线程外操作GUI控件,就需要使用ISynchronizeInvoke接口中的Invoke(同步执行)和BeginInvoke(异步执行)方法,通过一个委托把调用封送到控件所属的线程上执行。
通过上面的介绍,我们不得不思考一个问题,为什么Control类提供了Invoke和BeginInvoke方法?
关于这个问题,我主要从以下几个方面来进行讲解:
1.Windows程序消息机制
在Windows中,GUI程序是基于消息机制,有一个主线程维护着一个消息泵。这个主线程维护着整个窗体以及窗体上面的子控件。当它获取到消息时,就会调用DispatchMessage()方法派遣消息,会对窗体上的窗口过程进行调用(窗口过程里面由程序员提供的窗体数据更新代码以及其它的代码)。
Windows GUI程序的消息循环
GUI窗体上的所有消息是通过Windows程序的消息队列所提供。在GUI程序运行时,为了不断的从队列中获取消息,Windows程序在while循环中主要使用GetMessage()方法,这是个阻塞方法,当Windows消息队列为空时,此方法就会被阻塞,此时while循环就会停止(主要避免程序把CPU耗尽,使其它程序难以得到响应。若需要CPU进行最大限度的运行(如3D游戏),一般使用PeekMessage()方法,它不会被Windows阻塞,从而保证程序的流畅和比较高的帧速)。
2. .NET的消息循环
public static void Main(string[] args)
Form frm = new Form();
Application.Run(frm);
}
在上述代码中,.NET窗体封装了while循环, Application.Run (frm)方法开启循环过程。
3.消息机制---进程与线程间的通信
Windows消息发送
Windows消息机制是Windows平台上的线程或者进程间通信机制之一。Windows消息值的定义为一个数据结构,其中最重要的是消息类型,为一个整数;然后是消息参数,消息参数可以表达很多。
当然,Windows也提供了一些API用来向一个线程队列发送消息。因此,一个线程可以向另一个线程的消息队列发送消息,这样就实现了线程间的通信。有些API发送消息需要一个窗口句柄,这种函数可以把消息发送到指定窗口的主线程消息队列,而有些可直接通过线程句柄,把消息发送到该线程的消息队列中。
消息机制通信
Windows API中提供了SendMessage()方法,把一个消息发送到一个窗口的消息队列。这个方法为阻塞方法,操作系统会确保消息确实发送到目的消息队列,并且该消息被处理后,该方法才返回,并将控制权返还给调用者,在返回之前,调用者将暂时被阻塞。
PostMessage()方法也是发送消息到窗口的消息队列,由Windows API所提供,但这个方法是非阻塞,调用者在调用该方法后,会立即返回,操作系统不会确保消息是否的确发送到目的消息队列中,调用者也不会被阻塞。
Invoke和BeginInvoke方法
由上述可知,Invoke和BeginInvoke方法由ISynchronizeInvoke接口提供,且都需要一个委托对象作为参数。委托类似于回调函数的地址,调用者可通过这两个方法将需要调用的函数地址封送到界面线程。如果这些封送给界面线程的方法包含了更改界面控件的代码,由于最终执行这个方法是在界面控件线程中,从而避免了线程之间的竞争。
使用Invoke方法完成委托的封送,类似于使用SendMessage方法给界面线程发送消息,为同步方法。在Invoke封装的方法未执行完毕之前,Invoke方法是不会返回,使调用者线程被阻塞。
使用BeginInvoke方法完成委托的封送,类似于使用PostMessage方法给界面线程发送消息,为异步方法。在完成委托的封送后,BeginInvoke方法立即返回,不会等待委托的方法执行完毕,调用者线程将不会阻塞。但是调用者可以使用EndInvoke方法或者其它类似WaitHandle机制等待异步操作的完成。
但是,Invoke和BeginInvoke在内部的实现上都是使用PostMessage方法,从而避免SendMessage方法带来的问题。其中Invoke方法的同步阻塞是使用WaitHandle机制完成。
使用场合
若你的后台线程在更新UI界面且不需要等待,则应该使用BeginInvoke方法进行异步操作。
若你的后台线程在更新UI界面并且需要等待,则应该使用Invoke方法进行同步操作。
在ISynchronizeInvoke接口中,可以看到还有一个InvokeRequired。这个属性的作用是:在编程时确定一个对象在访问UI控件时是否需要使用Invoke和BeginInvoke方法进行封送,如果不需要则可以直接进行更新。在调用者对象和UI对象属于同一个线程时,该属性返回false。Control类对这一属性的实现是在判断调用者和控件是否同属于一个线程。
Delegate.BeginInvoke
通过一个委托来进行同步方法的异步调用,也是.NET提供的异步调用机制之一。但是Delegate.BeginInvoke方法是从ThreadPool中取出的一个线程来执行这个方法,以获得异步的执行效果。如果采用这种方式提交多个异步委托,这些调用的顺序无法得到保障。而且由于使用的是线程池中的线程来完成任务,若频繁的使用,会对系统的性能造成影响。
在Control类中的BeginInvoke方法没有开辟新的线程完成委托任务,而是让界面所属的控件线程完成委托任务(异步操作就是开辟新的线程的说法也不一定准确)。
Control.BeginInvoke和Control.Invoke
public IAsyncResult BeginInvoke(Delegate method, params object[] args)
using (new MultithreadSafeCallScope())
return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
public object Invoke(Delegate method, params object[] args)