异步编程(十一):委托方法的BeginInvoke和EndInvoke

委托对象的调用列表中只有一个方法(我们通常称之为 引用方法 )时,可以进行异步执行。委托类有两个方法:BeginInvoke和EndInvoke就是用来实现这一效果的。

  • 当我们调用委托的BeginInvoke方法时,将会在一个独立的线程执行委托的引用方法,并立即返回到原始线程(即调用方法的位置),继续向下执行。此时委托的引用方法在线程池中并行执行。
  • 当程序希望获取已完成的异步方法结果时,可以检查BeginInvoke返回的IAsycnResult的IsCompleted属性,或者调用委托的EndInvoke方法来等待委托的完成。
  • 通常我们在实际使用过程中,这种异步执行的方法常常采用三种模式:

  • 等待完成模式(wait-until-done):发起异步方法后,就去完成其他处理内容,然后原始线程就中断并等待异步方法完成后才继续其他执行。
  • 轮询模式(polling):原始线程通过循环定期检查异步方法是否完成,如果没有则继续做其他事情。
  • 回调模式(callback):原始线程发起异步方法后就不在理会和检查结果。异步方法在完成后,自行调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结果。
  • 1. BeginInvoke和EndInvoke方法

    首先我们需要了解BeginInvoke的一些重要事项:

  • BeginInvoke 参数的组成:
  • 委托的引用方法自身的参数;
  • 两个额外参数:callback 和state参数。
  • BeginInvoke从线程池中获得一个线程并让引用方法在此线程中开始运行。
  • BeginInvoke会返回一个实现IAsyncResult接口的对象,这个对象可以反映线程中异步方法的实时状态。
    delegate long MyDel(int first, int second);
    static long Sum (int x,int y){...}
    MyDel del=new MyDel(Sum);
    IAsyncResult iar = del.BeginInvoke(3,5,null,null);
    

    EndInvoke 用来获取异步方法调用返回的值,并且清除释放线程中使用的资源。特征如下:

  • 它接受BeginInvoke返回的IAsyncResult对象,并由此找到关联的线程;
  • 如果线程池中的线程已经退出,则做如下两件事:
  • 清理线程状态并释放其资源;
  • 如果引用方法有返回值,则将其返回。
  • 如果线程仍在运行,EndInvoke将停止并等待,直到方法完成,然后清理线程并获取返回值。因为EndInvoke负责线程清理工作,因此每一个BeginInvoke都必须有对应的EndInvoke相匹配。
  • 如果异步方法触发了异常,将在调用EndInvoke方法时抛出。
  • 如下代码示例了一个EndInvoke的调用,它必须以IAsyncResult对象的引用为参数:

    long result = del.EndInvoke(iar);
    

    EndInvoke提供了从异步方法返回的所有输出,包括ref和out参数。如果委托的引用方法有ref和out参数,它们就必须包含在EndInvoke的参数列表中。例如:

    long result = del.EndInvoke(out int someInt, iar);
    

    2. 等待完成模式

    在这种模式下,原始线程发起一个异步方法的调用,做一些事情处理,然后停止并等待,直到开启线程结束。

    using System;
    using System.Threading;
    delegate long MyDel (int first, int second);
    class Program
        static long Sum(int x, int y)
            Console.WriteLine(“        Inside Sum”);
            Thread.Sleep(100);
            return x+y;
        static void Main()
            MyDel del=new MyDel(Sum);
            Console.WriteLine(“Before BeginInvoke”);
            IAsyncResult iar=del.BeginInvoke(3, 5, null, null);
            Console.WriteLine(“ After BeginInvoke”);
            Console.WriteLine(“Doing stuff”);
            long result = del.EndInvoke(iar);
            Console.WriteLine(“After EndInvoke:{0}”, result);
    

    3.AsyncResult类

    AsyncResult 是方法的必要部分,BeginInvoke 返回一个IAsyncResult接口的引用(内部是AsyncResult类的对象)。AsyncResult类表现了异步方法的状态。

  • 当我们调用委托对象的BeginInvoke方法时,系统创建一个AsyncResult类的对象,然后它不返回类对象的引用,而是返回对象中包含的IAsyncResult接口的引用。
  • AsyncResult对象包含一个叫做AsyncDelegate的属性,它返回一个指向被调用来开启异步方法的委托的引用。注意,这个属性是类对象的一部分而不是接口的一部分。
  • IsCompleted 属性返回异步方法是否完成的布尔值。
  • AsyncState返回一个对象引用,做为BeginInvoke 方法调用时的state参数。它返回object类型的引用,只有在回调模式中使用。
  • 4. 轮询模式

    在轮询模式中,原始线程发起了异步方法的调用,做一些其他处理,然后使用IAsyncResult对象的IsCompleted属性定期检查开启的线程是否完成。如果完成则继续向下执行,否则继续做它的其他处理,直到下一次检查。

        delegate long MyDel(int x, int y);
        class program
            long Sum(int x, int y)
                Console.WriteLine("        Inside Sum");
                Thread.Sleep(100);
                return x + y;
            static void Main()
                MyDel del = new MyDel(Sum);
                IAsyncResult iar = del.BeginInvoke(3, 5, null, null);
                Console.WriteLine("After BeginInvoke");
                while (!iar.IsCompleted)
                    Console.WriteLine("Not Done");
                    for (long i = 0; i < 10000000; i++) ;
                Console.WriteLine("Done");
                long result = del.EndInvoke(iar);
                Console.WriteLine("Result:{0}", result);
    

    4. 回调模式

    在之前的等待结束模式和轮询模式中,初始线程都是继续自己的控制流程,直到它知道开启的线程已经完成,然后获取结果并继续。
    回调模式则与之不同,一旦初始线程发起了异步方法,由它自己管理,就不再进行管理。异步方法调用结束后,系统自动调用一个用户自定义的方法来自行处理结果,并且调用委托的EndInvoke方法。这个用户自定义的方法做做回调方法或简称回调
    BeginInvoke的参数列表中最后两个额外参数就是供回调方法使用的。

  • 第一个参数callback,是回调方法的名字;
  • 第二个参数state,可以是null或要传入回调方法的一个对象引用。我们可以通过使用IAsyncResult参数的AsyncState属性来获取这个对象,参数类型是object。
  • 1)回调方法

    回调方法的签名和返回类型必须与AsyncCallback委托类型所描述的形式一致。它需要方法接受一个IAsyncResult作为参数并且返回类型是void,如下所示

     void AsyncCallback(IAsysnResult iar)
    

    我们有多种方式可以为BeginInvoke方法提供回调方法。由于BeginInvoke中的callback参数是AsyncCallback类型的委托,我们可以以委托形式提供,或者,我们也可以只提供回调方法的名称,让编译器为我们创建委托,两种方法是完全等价的。

    IAsyncResult iar1=del.BeginInvoke(3,5,new AsyncCallback(CallWhenDone),null)
    IAsyncResult iar2=del.BeginInvoke(3,5,CallWhenDone,null)
    

    BeginInvoke的另外一个参数是发送给回调方法的对象。它可以是任何类型的对象,但是参数类型是object。所以在回调方法中,我们必须转换成正确的类型。

    2)在回调方法中调用EndInvoke

    在回调方法内,我们的代码应该调用委托的EndInvoke方法来处理异步方法执行后的输出值。要调用委托的EndInvoke方法,我们肯定需要委托对象的引用,而它在初始线程中,不在开启的线程中。如果不使用state参数作为其他用途,可以使用它发送委托的引用给回调方法。

    IAsyncResult iar = del,BeginInvoke(3,5,CallWhenDone,del);
    
  • 给回调方法的参数只有一个,就是刚结束的异步方法的IAsyncResult接口的引用。请记住:IAsyncResult接口对象在内部就是AsyncResult类对象。
  • 尽管IAsyncResult接口没有委托对象的引用,而封装它的AsyncResult类对象却有委托对象的引用。所以可以通过转换接口引用为类类型来获取类对象的引用。
  • 有了类对象的引用,我们现在就可以调用类对象的AsyncDelegate属性并且把它转换为合适的委托类型。这样就得到了委托引用,我们可以用它来调用EndInvoke方法。
  • using System.Runtime.Remoting.Messaging;        //包含AsyncResult类
            void CallWhenDone(IAsyncResult iar)
                AsyncResult ar = (AsyncResult)iar;      // 获取类对象的引用
                MyDel del = (MyDel)ar.AsyncDelegate;    // 获取委托的引用
                long sum = del.EndInvoke(iar);          // 调用EndInvoke方法
    

    如此我们就完成了一个使用回调模式的完整示例。为了方便阅读,我们把所有知识点放在一起:

    using System;
    using System.Threading;
    using System.Runtime.Remoting.Messaging; //包含AsyncResult类
    namespace YbydjyLibrary
        delegate long MyDel(int first, int second);
        class Program
            static long Sum(int x, int y)
                Console.WriteLine("            Inside Sum");
                Thread.Sleep(100);
                return x + y;
            static void CallWhenDone(IAsyncResult iar)
                Console.WriteLine("            Inside CallWhenDone");
                AsyncResult ar = (AsyncResult)iar;      // 获取类对象的引用
                MyDel del = (MyDel)ar.AsyncDelegate;    // 获取委托的引用
                long sum = del.EndInvoke(iar);          // 调用EndInvoke方法
                Console.WriteLine($"Result:{sum}");
            static void Main()
                MyDel del = new MyDel(Sum);
                Console.WriteLine("Before BeginInvoke");
                IAsyncResult iar = del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);
                Console.WriteLine("Doing more work in Main");
                Thread.Sleep(500);