大马哈鱼的C#学习笔记(3):Invoke/BeginInvoke/DynamicInvoke

时间:2021-11-24 04:13:23

Control.Invoke和Control.BeginInvoke

Control.Invoke()是同步方法,它会往Control所处的线程(UI主线程)消息队列中加一条消息,请求执行某个委托,在该委托方法(可能多个)执行完毕后,Control.Invoke()得以返回,继续执行下一行代码。当委托的InvocationList元素有多个时,即有多个方法需要执行时,Control.Invoke()返回值是最后一个执行的方法的返回值。

Control.BeginInvoke是异步方法,它往Control所处的线程(UI主线程)消息队列中加一条消息,请求执行某个委托,然后立刻返回。至于请求的方法何时开始执行,它不关心。但是请求的方法执行的结果,它必然要关心,所以Control.BeginInvoke()有一个IAsyncResult的返回值,这个IAsyncResult可以看作委托的凭据。Control.BeginInvoke()调用后,可以在任意时间后调用Control.EndInvoke(IAsyncResult)来获得该凭据对应的执行结果。如果尚未执行完,则一直在那等,直到执行完成为止。EndInvoke的返回值是一个object,即为委托方法的返回值(同样地,如果有多个委托方法,则为最后一个方法的返回值)。

值得注意的是,EndInvoke只能被调用一次,否则会引发异常。

在调用Control.Invoke/BeginInvoke之前,可以先使用Control.InvokeRequired检测是否调用者当前线程跟Control创建线程不同,如果不同,则表示必须使用Invoke来调用对Control进行操作的方法,否则可以直接调用。

以下代码可以用来演示Control.InvokeRequired的使用:

        private delegate int AddDel(int value);

        private AddDel addDel = (value) =>

            {

                return ++value;

            };

        private void button1_Click(object sender, EventArgs e)

        {

            Button button = sender as Button;

                            if(button.InvokeRequired)        //一定为false

{

                                     //blabla

}

                            //请求使用线程池中的线程来执行workThreadMathod

                            ThreadPool.QueueUserWorkItem(workThreadMathod, sender);

        }

        private void workThreadMathod(object obj)

        {

            Button button = obj as Button;

            if (button.InvokeRequired)       //一定为 true

            {

                //blabla

            }

        }

Control.BeginInvoke返回的IAsyncResult不能强转为AsyncResult,AsyncResult类只用于Delegate的BeginInvoke。

IAsyncResult. AsyncWaitHandle.WaitOne()如果被调用,则一直会等到委托方法执行完毕后返回,这应该就是EndInvoke的内部实现。但是它也开放出来给用户使用了,因为有些场合,既需要等候委托方法的完成,又需要在取得委托方法的返回值之前(调用EndInvoke)干点别的什么事,那么就需要手动调用WaitOne()了。当然,如果没有这种变态需求,直接调用EndInvoke即可。IAsyncResult. AsyncWaitHandle.WaitOne()使用后,需要调用Close()来关闭句柄。

有一点需要注意的是,如果Control.BeginInvoke是在创建Control的线程(主线程)中被调用,那么一定不能使用IAsyncResult. AsyncWaitHandle.WaitOne(),因为这样会造成死锁,调用处在无限等待,委托方法又得不到执行,线程就一直被挂起了。

Delegate.Invoke 和 Delegate.BeginInvoke

跟Control.Invoke、Control.BeginInvoke类似的,前者是同步方法,后者是异步方法。

Delegate.Invoke使用起来就跟直接调用方法类似,参数就是委托的参数类型。

也可以使用完全跟方法调用一样的形式,即加括号()发方式来调用,内部实现上依旧是调用的Invoke方法。

Delegate.Invoke除了是同步的以外,还有一个显著特征是,委托方法执行在调用处同一个线程中。

Delegate.BeginInvoke则不同,它除了是异步的以外,还有个很重要的特征,它从线程池中抓取一个空闲线程,来执行委托方法。

看一下Delegate.BeginInvoke的签名:

public class xxxDelegate : MulticastDelegate

{

         public IAsyncResult BeginInvoke(...,AsyncCallback cb, object @object);

}

...处是委托方法的参数列表,cb是一个AsyncCallback实例,AsyncCallback的定义如下:

public delegate void AsyncCallback(IAsyncResult ar);

@object是调用者自己传入的对象,到时候在AsyncCallback委托的方法中可以获取到。

下面结合实例讲一下BeginInvoke的使用流程:

        private delegate int AddDel(int value);

        private AddDel addDel = (value) =>

            {

               return ++value;

            };

        private void button1_Click(object sender, EventArgs e)

        {

            IAsyncResult iar = addDel.BeginInvoke(33, (v) =>          //1

            {

                AsyncResult ar = v as AsyncResult;                            //2

                AddDel del = ar.AsyncDelegate as AddDel;     //3

                string inputObj = ar.AsyncState as string;       //4

                if (!ar.EndInvokeCalled)                                        //5

                {

                    int result = (int)del.EndInvoke(ar);           //6

                }

            },@"abc");                                                                         //7

            int result1 = addDel.EndInvoke(iar);                           //8

        }

为了方便序数,以上事例各行都编了号。

第1行,请求线程池安排一个空闲线程来执行addDel的委托方法,参数为33

第2~6行,使用一个lambda表达式作为BeginInvoke的AsyncCallback参数,表示说在addDel的委托方法返回后,需要执行该AsyncCallback实例的委托方法,即执行花括号内的代码块。注意,这个代码块运行于addDel委托方法同一个线程中。

第2行,将IAsyncResult类型的v强转成AsyncResult对象,这个跟Control.BeginInvoke的返回值不同,只有Delegate.BeginInvoke的返回值才是一个AsyncResult实例。

同时,ar和第1行的iar实际上是同一个引用。

第3行,从ar中拿到委托实例,del和addDel实际上是同一个引用。

第4行,从ar中拿到传入的对象,即第7行的@”abc”。

第5行,判断EndInvoke是否已经调用过了。因为EndInvoke只能调用一次,所以提供这个方法来提前判断。

第6行,调用EndInvoke来获取addDel委托方法的返回值。

第8行,这行代码所处的线程和2~6行所处的线程是不同的,因此实际上是无法知道到底第8行跟第6行哪一个EndInvoke会真正执行,而且,极端的情况下,如果第6行得到执行的话,那第8行是会引发异常的,同样,如果第8行执行完后,第6行所处的线程得到调度,那第6行也会引发异常。第5行的EndInvokeCalled理论上在这里毫无作用。

以上只是演示了Delegate.BeginInvoke的使用。实际的编码过程中,应该要避免在Delegate.BeginInvoke的调用线程以及AsyncCallback委托方法中同时进行EndInvoke的处理逻辑。

至于到底把后续逻辑放在哪一块来执行,就得看需求而定了,如果是耗时的后台操作,那么放在AsyncCallback的委托方法中比较好;如果addDel.BeginInvoke调用处本身也是一个工作线程,那似乎放到第8行以后来处理会比较好,毕竟可以不用切换上下文环境。

Delegate.DynamicInvoke

跟Delegate.Invoke类似,同步,且同线程,唯一不同的是,是采用后期绑定的方式来调用委托方法。

假如一个Delegate(注意D是大小的,表示类实例),必须要到运行时动态来确定其方法签名(即该Delegate实例是通过Delegate.CreateDelegate()动态创建的),那就要用到DynamicInvoke()。查看Delegate类的结构,发现DynamicInvoke是一个public方法,同时还有protected virtual object DynamicInvokeImpl(object[] args);这么一个虚方法。猜测通过delegate关键字定义的类,应该是重写了这个虚方法。

那么如果对一个通过Delegate.CreateDelegate()动态创建的Delegate实例调用Invoke会怎样?答案是没有这回事,因为Delegate根本就没有Invoke方法。

对于通过delegate关键字定义的Delegate类,由于编译期间已经确定了其方法签名,那么用Invoke或者DynamicInvoke,效果是一样的。当然,由于DynamicInvoke在编译时不检查方法签名是否匹配,如果在运行时发现不匹配的话,会引发异常。Invoke由于在编译期间就已经确保了签名匹配,所以运行期间不会引发参数不匹配异常。

WP7中的BeginInvoke

目前版本的wp7中,Control.Invoke以及Control.BeginInvoke都不见了踪影。

Delegate.Invoke可用,而Delegate.BeginInvoke在运行时却会抛异常。所以很令人沮丧。

目前我找到的唯一可用的BeginInvoke,位于System.Windows.Threading.Dispatcher.BeginInvoke。

在Silverlight 4.0 for WP中,任何一个DependencyObject都有一个Dispatcher属性,可以获取到代表当前主线程的Dispatcher对象,调用其BeginInvoke即可呼叫主线程来执行提供的委托。

不过其返回值DispatcherOperation对象是没有任何实现的,简直坑爹呀。