Invoke与BeginInvoke-阿里云开发者社区

开发者社区> 开发与运维> 正文

Invoke与BeginInvoke

简介:   一、为什么 Control类提供了 Invoke和 BeginInvoke机制? 关于这个问题的最主要的原因已经是 dotnet程序员众所周知的,我在此费点笔墨再次记录到自己的日志,以便日后提醒一下自己。

 

一、为什么 Control类提供了 Invoke和 BeginInvoke机制?

关于这个问题的最主要的原因已经是 dotnet程序员众所周知的,我在此费点笔墨再次记录到自己的日志,以便日后提醒一下自己。

1、 windows程序消息机制

Windows GUI程序是基于消息机制的,有个主线程维护着一个消息泵。这个消息泵让 windows程序生生不息。

                                                  Windows GUI 程序的消息循环 

 

Windows程序有个消息队列,窗体上的所有消息是这个队列里面消息的最主要来源。这里的 while循环使用了 GetMessage()这个方法,这是个阻塞方法,也就是队列为空时方法就会被阻塞,从而这个 while循环停止运动,这避免了一个程序把 cpu无缘无故地耗尽,让其它程序难以得到响应。相反的还有一对当然在某些需要 cpu最大限度运动的程序里面就可以使用另外的方法,例如某些 3d游戏或者及时战略游戏中,一般会使用 PeekMessage()这个方法,它不会被 windows阻塞,从而保证整个游戏的流畅和比较高的帧速。

这个主线程维护着整个窗体以及上面的子控件。当它得到一个消息,就会调用 DispatchMessage方法派遣消息,这会引起对窗体上的窗口过程的调用。窗口过程里面当然是程序员提供的窗体数据更新代码和其它代码。

 

 

2、 dotnet里面的消息循环

public static void Main(string[] args)

{

   Form f = new Form();

   Application.Run(f);

}

Dotnet窗体程序封装了上述的 while循环,这个循环就是通过 Application.Run方法启动的。

3、线程外操作 GUI控件的问题

如果从另外一个线程操作 windows窗体上的控件,就会和主线程产生竞争,造成不可预料的结果,甚至死锁。因此 windows GUI编程有一个规则,就是只能通过创建控件的线程来操作控件的数据,否则就可能产生不可预料的结果。

因此, dotnet 里面,为了方便地解决这些问题, Control 类实现了 ISynchronizeInvoke 接口,提供了 Invoke 和 BeginInvoke 方法来提供让其它线程更新 GUI 界面控件的机制。

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 ; }

}

}

如果从线程外操作 windows窗体控件,那么就需要使用 Invoke或者 BeginInvoke方法,通过一个委托把调用封送到控件所属的线程上执行。

二、消息机制 ---线程间和进程间通信机制

1、 window消息发送

Windows消息机制是 windows平台上的线程或者进程间通信机制之一。 Windows消息值其实就是定义的一个数据结构,最重要的是消息的类型,它就是一个整数;然后就是消息的参数。消息的参数可以表示很多东西。

Windows 提供了一些 api 用来向一个线程的消息队列发送消息。因此,一个线程可以向另一个线程的消息队列发送消息从而告诉对方做什么,这样就完成了线程间的通信。有些 api 发送消息需要一个窗口句柄,这种函数可以把消息发送到指定窗口的主线程消息队列;而有些则可以直接通过线程句柄,把消息发送到该线程消息队列中。

                                                   

用消息机制通信

 

SendMessage是 windows api,用来把一个消息发送到一个窗口的消息队列。这个方法是个阻塞方法,也就是操作系统会确保消息的确发送到目的消息队列,并且该消息被处理完毕以后,该函数才返回。返回之前,调用者将会被暂时阻塞。

PostMessage也是一个用来发送消息到窗口消息队列的 api函数,但这个方法是非阻塞的。也就是它会马上返回,而不管消息是否真的发送到目的地,也就是调用者不会被阻塞。

2、 Invoke and BeginInvoke

 

                                                        Invoke or BeginInvoke

 

Invoke或者 BeginInvoke方 法都需要一个委托对象作为参数。委托类似于回调函数的地址,因此调用者通过这两个方法就可以把需要调用的函数地址封送给界面线程。这些方法里面如果包含了 更改控件状态的代码,那么由于最终执行这个方法的是界面线程,从而避免了竞争条件,避免了不可预料的问题。如果其它线程直接操作界面线程所属的控件,那么 将会产生竞争条件,造成不可预料的结果。

使用 Invoke完成一个委托方法的封送,就类似于使用 SendMessage方法来给界面线程发送消息,是一个同步方法。也就是说在 Invoke封送的方法被执行完毕前, Invoke方法不会返回,从而调用者线程将被阻塞。

使用 BeginInvoke方法封送一个委托方法,类似于使用 PostMessage进行通信,这是一个异步方法。也就是该方法封送完毕后马上返回,不会等待委托方法的执行结束,调用者线程将不会被阻塞。但是调用者也可以使用 EndInvoke方法或者其它类似 WaitHandle机制等待异步操作的完成。

但是在内部实现上, Invoke和 BeginInvoke都是用了 PostMessage方法,从而避免了 SendMessage带来的问题。而 Invoke方法的同步阻塞是靠 WaitHandle机制来完成的。

 

 先来看看Send与Post这两个英文单词的意思:Send有发送的意思,而Post具有投寄的意思。
      Send:  相当于邮寄员,他会将快件亲手交给收件人,并且需要收件人签字,他才闪人。而在消息机制中,就是说,系统(邮寄员)会将收到的消息(邮局分发)直接发送到某个窗口的窗口过程(收件人),并且需要该窗口作出处理(收件人签字)才返回。 这东东就是SendMessage
      Post:  相当于邮局、邮筒等等,我们写好信好,会将信交给邮局,或投寄到邮筒里,而什么时候发送,发送到哪里都由邮局来处理,我们投寄信件的时候,是不会等候这封信件到达收件人手里,然后才回家的。 而在消息机制中,就是说,系统(我们)将收到的消息(信件)投寄到应用程序的消息循环(相当于邮筒)中,然后就闪人,具体啥时候处理这条消息(啥时候发送邮件),那就得看“办事效率”了。
      区别很明显,SendMessage的消息是不进队列的,而PostMessage的需要排队。 
      但,值得说明的是:虽然一个要进队,一个不进队,但是最终处理消息的地方都一样:都是系统调用窗口过程进行处理(收件人作出反应)
      
    PreTranslateMessage
     我们可以PreTranslateMessage来对消息预处理,该用的用,不该用的不用。那么,是否Send和Post来的消息我们都可以用它来预处理呢?
     答案当然是否定的:如果你在深圳,要写封信寄到东北去,那么投寄方式有两种:1、自己带着信跑到东北去,2、将信放到邮局,由邮局分发。
     第1 种情况下,就是SendMessage了,这个时候,还有谁能预处理你的信呢?
     第2种情况下,就是PostMessage了,这个时候,嘿嘿,万一运气不好,被邮局给用PreTranslateMessage给截了.....
     所以说,用SendMessage发送的消息是不能用PreTranslateMessage来预处理的。而Post的就可以,因为它要经过“第三者”。

3、使用场合问题

如果你的后台线程在更新一个 UI控件的状态后不需要等待,而是要继续往下处理,那么你就应该使用 BeginInvoke来进行异步处理。

如果你的后台线程需要操作 UI控件,并且需要等到该操作执行完毕才能继续执行,那么你就应该使用 Invoke。否则,在后台线程和主截面线程共享某些状态数据的情况下,如果不同步调用,而是各自继续执行的话,可能会造成执行序列上的问题,虽然不发生死锁,但是会出现不可预料的显示结果或者数据处理错误。

可以看到 ISynchronizeInvoke有一个属性, InvokeRequired。这个属性就是用来在编程的时候确定,一个对象访问 UI控件的时候是否需要使用 Invoke或者 BeginInvoke来进行封送。如果不需要那么就可以直接更新。在调用者对象和 UI对象同属一个线程的时候这个属性返回 false。在后面的代码分析中我们可以看到, Control类对这一属性的实现就是在判断调用者和控件是否属于同一个线程的。

三、 Delegate.BeginInvoke

通过一个委托来进行同步方法的异步调用,也是 .net提供的异步调用机制之一。但是 Delegate.BeginInvoke方法是从 ThreadPool取出一个线程来执行这个方法,以获得异步执行效果的。也就是说,如果采用这种方式提交多个异步委托,那么这些调用的顺序无法得到保证。而且由于是使用线程池里面的线程来完成任务,使用频繁,会对系统的性能造成影响。

Delegate.BeginInvoke也是讲一个委托方法封送到其它线程,从而通过异步机制执行一个方法。调用者线程则可以在完成封送以后去继续它的工作。但是这个方法封送到的最终执行线程是运行库从 ThreadPool里面选取的一个线程。

这里需要纠正一个误区,那就是Control类上的异步调用BeginInvoke并没有开辟新的线程完成委托任务,而是让界面控件的所属线程完成委托任务的。看来异步操作就是开辟新线程的说法不一定准确。  

 

版权声明:本文首发在云栖社区,遵循云栖社区版权声明:本文内容由互联网用户自发贡献,版权归用户作者所有,云栖社区不为本文内容承担相关法律责任。云栖社区已升级为阿里云开发者社区。如果您发现本文中有涉嫌抄袭的内容,欢迎发送邮件至:developer2020@service.aliyun.com 进行举报,并提供相关证据,一经查实,阿里云开发者社区将协助删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章