C#窗体中Invoke和BeginInvoke方法详解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
在Invoke或者Beginlnvoke的使用中无一例外地使用了委托Delegate,至于
委托的本质请参考我的另一随笔:
一、为什么Control 类提供了Invoke 和Beginlnvoke机制?
1、windows 程序消息机制
WindowsGUl 程序是基于消息机制的,有个主线程维护着一个消息泵。
这个消息泵让windows 程序生生不息。
Win dows GUI程序的消息循环
Windows 程序有个消息队列,窗体上的所有消息是这个队列里面消息的最主要来源。
这里的while循环使用了GetMessage ()这个方法,这是个阻塞方法,也就是队列为空时方法就会被阻塞,从而这个while 循环停止运动,这避免了一个程序把cpu 无缘无故地耗尽,让其它程序难以得到响应。
当然在某些需要cpu最大限度运动的程序里面就可以使用另外的方法,例如某些3d游戏或者
及时战略游戏中,一般会使用PeekMessage ()这个方法,它不会被windows 阻塞,从而保证整个游戏的流畅和比较高的帧速。
这个主线程维护着整个窗体以及上面的子控件。
当它得到一个消息,就会调用DispatchMessage方法派遣消息,这会引起对窗体上的窗口过程的调用。
窗口过程里面当然是程序员提供的窗体数据更新代码和其它代码。
public static void Main(string[] args){Form f = new Form();
3、线程外操作GUI 控件的问题
如果从另外一个线程操作windows 窗体上的控件,就会和主线程产生竞争,造成不可预料的结果,甚至死锁。
因此win dows GUI编程有一个规则,就是只能通过创建控件的线程来操作控件的数据,否则就可能产生不可预料的结果。
public interface
ISynchronizeInvoke{[HostProtection(SecurityAction.LinkDemand,Synchronization=tru e,ExternalThreading=true)]
IAsyncResult BeginInvoke(Delegate method, object[] args);
object EndInvoke(IAsyncResult result);
object Invoke(Delegate method, object[] args);
bool InvokeRequired { get; }}}
如果从线程外操作windows 窗体控件,那么就需要使用Invoke 或者
Begi nln voke方法,通过一个委托把调用封送到控件所属的线程上执行。
二、消息机制---线程间和进程间通信机制
1、window 消息发送
Windows 消息机制是windows 平台上的线程或者进程间通信机制之一。
Windows 消息值其实就是定义的一个数据结构,最重要的是消息的类型,它就是一个整数;然后就是消息的参数。
消息的参数可以表示很多东西。
Windows 提供了一些api 用来向一个线程的消息队列发送消息。
因此,一个线程可以向另一个线程的消息队列发送消息从而告诉对方做什么,这样就完成了线程间的通信。
有些api发送消息需要一个窗口句柄,这种函数可以把消息发送到指定窗口的主线程消息队列;而有些则可以直接通过线程句柄,把消息发送到该线程消息队列中。
用消息机制通信
SendMessage是windows api,用来把一个消息发送到一个窗口的消息队列。
这个方法是个阻塞方法,也就是操作系统会确保消息的确发送到目的消息队列,并且该消息被处理完毕以后,该函数才返回。
返回之前,调用者将会被暂时阻塞。
PostMessage也是一个用来发送消息到窗口消息队列的api函数,但这个方
法是非阻塞的。
也就是它会马上返回,而不管消息是否真的发送到目的地,也就是调用者不会被阻塞。
2、Invoke and BeginInvoke
Invoke or BeginInvoke
Invoke或者Beginlnvoke方法都需要一个委托对象作为参数。
委托类似于回调函数的地址,因此调用者通过这两个方法就可以把需要调用的函数地址封送给界面线程。
这些方法里面如果包含了更改控件状态的代码,那么由于最终执行这个方法的是界面线程,从而避免了竞争条件,避免了不可预料的问题。
如果其它线程直接操作界面线程所属的控件,那么将会产生竞争条件,造成不可预料的结果。
使用Invoke完成一个委托方法的封送,就类似于使用SendMessage方法来给界面线程发送消息,是一个同步方法。
也就是说在Inv oke封送的方法被执行完毕前,Invoke方法不会返回,从而调用者线程将被阻塞。
使用Beginlnvoke方法封送一个委托方法,类似于使用PostMessage进行通信,这是一个异步方法。
也就是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。
但是调用者也可以使用Endlnvoke方法或者其它类似WaitHandle机制等待异步操作的完成。
但是在内部实现上,Invoke和BeginInvoke都是用了PostMessage方法,从而避免了SendMessage带来的问题。
而Invoke方法的同步阻塞是靠WaitHandle 机制来完成的。
3、使用场合问题
如果你的后台线程在更新一个UI控件的状态后不需要等待,而是要继续往下处理,那么你就应该使用Begi nInv oke来进行异步处理。
如果你的后台线程需要操作UI 控件,并且需要等到该操作执行完毕才能继续执行,那么你就应该使用In voke。
否则,在后台线程和主截面线程共享某些状态数据的情况下,如果不同步调用,而是各自继续执行的话,可能会造成执行序列上的问题,虽然不发生死锁,但是会出现不可预料的显示结果或者数据处理错误。
可以看到ISynchronizelnvoke有一个属性,InvokeRequired。
这个属性就是用来在编程的时候确定,一个对象访问UI控件的时候是否需要使用Invoke或者
Begi nlnv oke来进行封送。
如果不需要那么就可以直接更新。
在调用者对象和UI
对象同属一个线程的时候这个属性返回false。
在后面的代码分析中我们可以看
到,Control 类对这一属性的实现就是在判断调用者和控件是否属于同一个线程的。
三、Delegate.BeginInvoke
Delegate.Begi nIn voke也是讲一个委托方法封送到其它线程,从而通过异步机制执行一个方法。
调用者线程则可以在完成封送以后去继续它的工作。
但是这个方法封送到的最终执行线程是运行库从ThreadPool里面选取的一个线程。
这里需要纠正一个误区,那就是Control类上的异步调用BeginInvoke并没有开辟新的线程完成委托任务,而是让界面控件的所属线程完成委托任务的。
看来异步操作就是开辟新线程的说法不一定准确。
四、用Reflector 察看一些相关代码
1、Control.BeginInvoke and Control.Invoke
publicIAsyncResultBeginInvoke(Delegatemethod, paramsobject[] args){using (newMultithreadSafeCallScope()){return(IAsyncResult)this.FindMarshalingControl(). MarshaledInvoke(this,method,args, false);}}
publicobjectInvoke(Delegatemethod, paramsobject[] args){using (newMultithreadSafeCallScope()){return
this.Fi ndMarshali ngCo ntrol().Marshaledl nv oke(this, method, args, true);}}这里的FindMarshalingControl 方法通过一个循环向上回溯,从当前控件开始回溯父控件,直到找到最顶级的父控件,用它作为封送对象。
例如,我们调用窗体上一个进度条的Invoke方法封送委托,但是实际上会回溯到主窗体,通过这个控件对象来封送委托。
因为主窗体是主线程消息队列相关的,发送给主窗体的消息才能发送到界面主线程消息队列。
我们可以看到Invoke和BeginInvoke方法使用了同样的实现,只是
MarshaledI nvoke方法的最后一个参数值不一样。
2、MarshaledInvoke
privateobjectMarshaledInvoke(Controlcaller,Delegatemethod,object[]args,bools ynchronous){intnum;
if (!this.IsHandleCreated){throw
newInvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));}
if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) !=
null){IntSecurity.UnmanagedCode.Demand();}boolflag= false;
if((SafeNativeMethods.GetWindowThreadProcessId(newHandleRef(this,this.Han dle),out num) ==SafeNativeMethods.GetCurrentThreadId()) && synchronous){flag = true;}ExecutionContexecutionContext= null;
if (!flag){executionContext
=ExecutionContext.Capture();}ThreadMethodEntryentry=
newThreadMethodEntry(caller, method, args, synchronous,executionContext);
lock (this){if (this.threadCallbackList== null){this.threadCallbackList= newQue();}}
lock (this.threadCallbackList){if (threadCallbackMessage==
0){"_ThreadCallbackMessage");}this.threadCallbackList.Enque(entry);}if
(flag){this.InvokeMarshaledCallbacks();
}threadCallbackMessage=+SafeNativeMethods.RegisterWindowMessage(Applic ation.WindowMessagesVersionelse
{// 终于找到你了,PostMessage}if (!synchronous) //如果是异步,那么马上返回吧{return entry;}if (!entry.lsCompleted)//同步调用没结束,阻塞起来等待吧
{this.WaitForWaitHandle(entry.AsyncWaitHandle);}if (entry.exception!= null){throw entry.exception;}return entry.retVal;}怎么样,我们终于看到PostMessage了吧?通过windows 消息机制实现了封送。
而需要封送的委托方法作为消息的参数进行了传递。
关于其它的代码这里不作进一步解释。
3、l nvokeRequi red
publicboollnvokeRequired{get{using
(newMultithreadSafeCallScope()){HandleRefref2;
intnum;
if (this.lsHandleCreated){ref2 = newHandleRef(this, this.Handle);}else
{UnsafeNativeMethods.PostMessage(newHandleRef(this,this.Handle),threadCall backMessage,lntPtr.Zero,lntPtr.Zero);
Controlwrapper= this.FindMarshalingControl();
if (!wrapper.lsHandleCreated){return false;}ref2 = newHandleRef(wrapper, wrapper.Handle);}intwindowThreadProcessld=SafeNativeMethods.GetWindowThrea dProcessld(ref2,out num);
intcurrentThreadld=SafeNativeMethods.GetCurrentThreadld();
retur n (wi ndowThreadProcessId != curre ntThreadld);}}}终于看至U了,这是在判断windows 窗体线程和当前的调用者线程是否是同一个,如果是同一个就没有必要封送了,直接访问这个GUI控件吧。
否则,就不要那么直接表白了,就需要Invoke 或者Beginlnvoke做媒了。