.NET异步程序设计——异步委托

shanzm-2020年2月11日 18:55:50

1.AMP模式简介

在.net1.x的版本中就可以使用IAsyncResult接口实现异步操作,但是比较复杂,这种称之为异步编程模型模式 (Asynchronous Programming Model, APM),也称为IAsyncResult模式

这种APM模式中一个同步操作XXX需要定义BeginXXX方法和EndXXX方法。

例如,如果有一个同步方法DownloadString,其异步版本就是BeginDownloadString和EndDownloadString方法。

BeginXXX方法接受其同步方法的所有输入参数,EndXXX方法使用同步方法的所有输出参数,并按照同步方法的返回类型来返回结果。

BeginXXX方法返回IAsyncResult接口的引用(内部是AsyncResult对象),用于验证调用是否已经完成,并且一直等到方法的执行结束。

使用异步模式时,BeginXXX方法还定义了一个AsyncCallback参数,用于接受在异步方法执行完成后调用的委托。

很麻烦,很不方便,实际开发中,.net 项目几乎不再使用这种方式实现异步操作(因为有更加方便的方法)。

所以自己基于APM模式去实现一个方法的异步版本,在这里不详细叙述

但是.net中一些对象的操作是默认实现了异步操作的,比如说:FileStream类中提供了BeginRead和EndRead来对文件进行异步字节读取操作(当然现在MSDN中推荐使用ReadAsync来替代!)。
使用起来有些坑,不详细写于此了,可以看点击:示例

还有 winform中的跨线程更新UI的方式一般就是使用Control类的BeginInvoke



2.使用BeginInvoke实现异步委托

基于AMP模型的委托异步编程还是相对比较方便的:(但是在 .Net Core 里也是已经不推荐使用了)

C#中委托具有异步性,支持异步调用(基于APM模型),即委托类型的对象不仅有调用同步方法的Invoke(),而且还定义了Beginlnvoke方法和Endlnvolve方法,用于使用异步模式。

这里先回顾一下委托,委托可以参考我的博文:C#-委托。看下面一个例子:

示例:委托的同步调用方法

static void Main(string[] args)
{
    Func<int, int, int> operateAdd = (int num1, int num2) =>
    {
        Console.WriteLine($"正在执行的线程,线程ID:{Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(5000);
        return num1 + num2;
    };
    Console.WriteLine($"正在执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}DoSomethingBeforeInvoke");
    int sum = operateAdd.Invoke(1, 2);//等价于:operateAdd(1, 2);
    Console.WriteLine("运算结果"+sum);
    //因为Invoke()是同步操作, 同步调用Add(),所以我们要等待5s
    Console.WriteLine($"正在执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}DoSomethingAfterInvoke");
    Console.ReadKey();
}

调试上面的程序你会发现,只有一个线程(即主函数Main()创建的主线程),所以在执行到需要长时间的操作operateAdd()的时候,整个程序都在等待它!

下面使用BeginInvoke()和EndInvoke()实现异步委托

首先使用BeginInvoke()调用需要异步执行方法(这个被调用的方法就是称之为引用方法),BeginInvoke()它会从线程池中获取一个新线程(即创建一个次线程)并在该线程执行引用方法,

并且立即返回到原始线程(即主线程,且这个原始线程又称为调用线程),从而原始线程可以继续执行,而引用方法会在线程池的新线程中并行执行。

返回值是IAsyncResult接口的引用,(其内部是AsyncResult类型的对象,这一点很重要!),该对象存放着新线程的有关信息,具体有四个属性,你可以通过VS F12转到定义自行查看,

这里列举两个常用的属性:

  • IsCompleted属性:可以查看异步操作是否完成,

  • AsyncWaitHandle属性:该属性返回一个WaitOne()方法,可以设置等待的最长时间,返回值是bool类型,如果指定时间为0,表示不等待,如果为-1,表示永远等待,直到异步调用完成。

之后使用EndInvoke()操作AsyncResult类型对象,获取异步操作的结果,同时释放次线程使用的资源。

其中EndInvoke()就只有一个参数,就是BeginInvoke()返回的AsyncResult类型对象。

注意原始线程中一旦运行到EndInvoke()后,原始线程则会停下来,等待BeginInvoke()运行的新线程运行完毕,返回引用方法的返回值。换言之:如果异步调用未完成,EndInvoke将一直阻塞调用线程,直到到异步调用完成。(这里就应该思考怎么避免这种阻塞!具体看后续:AsyncCallBack委托的作用)

示例:委托的异步调用方法

static void Main(string[] args)
{
    Func<int, int, int> operateAdd = (int num1, int num2) =>
    {
        Console.WriteLine($"正在执行的线程,线程ID{Thread.CurrentThread.ManagedThreadId}:执行异步委托中");
        Thread.Sleep(5000);
        return num1 + num2;
    };
    Console.WriteLine($"正在执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:DoSomethingBeforeInvoke");
    IAsyncResult result = operateAdd.BeginInvoke(1, 2,null, null);//此处最后两个参数必须是System.AsyncCallback和System.Object类型的对象,暂时按下不表,下面我会详细说明的
    while (!result.IsCompleted)//这里使用IAsyncResult类型对象的IsCompleted属性,用于判断是否完成BeginInvoke()
    {
        Thread.Sleep(1000);
        Console.WriteLine($"继续执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:……");
    }
    int sum = operateAdd.EndInvoke(result);
    Console.WriteLine("异步操作结果" + sum);
    Console.WriteLine($"正在执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:DoSomethingAfterInvoke");
    Console.ReadKey();
}

调试是可以发现,开始的时候运行Main()创建的主线程,之后运行到BeginInvoke()后创建了一个次线程,因为BeginInvoke()在后台继续运行,在它未结束之前继续运行主线程,当BeginInvoke()结束后则,result.IsCompleted此时为true,结束循环,打印异步操作的结果,继续主线程,运行如下:

异步委托运行结果

说明1

IAsyncResult类型的对象还有一个AsyncWaitHandle属性,该属性返回一个WaitOne()方法,可以设置等待的最长时间

如果超时则返回flase,在这里就可以继续运行主线程了,如果在等待时间之前次线程中的操作完成了,则在这里运行次线程中的操作。

    while (!result.AsyncWaitHandle.WaitOne(3000, true))//等待3s,在这里3s的等待中operateAdd()是完不成的,所以还是会先继续主线程操作
    {
        Console.WriteLine($"继续执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:……");
    }

说明2

在看书的过程中发现:

《精通C#(第6版)》P571:说明:“如果异步调用一个无返回值的方法,仅仅调用BeginInvoke()就可以了。在这种情况下,我们不需要缓存IAsyncResult兼容对象,也不需要首先调用EndInvoke()(因为没有收到返回值)。”

《C#5.0图解教程》P432:说明:“因为EndInvoke是为开启的线程进行清理,所以必须确保对每一个BeginInvoke都调用EndInvoke。”

两本书中对此的观点不一样,参考:博客园:关于《精通C#(第6版)》与《C#5.0图解教程》中的一点矛盾的地方

其实呀,简而言之,调用EndInvoke一定没坏处!

我的理解就是,在没有返回值的引用函数时实现异步,不使用EndInvoke,

就是相当于async & await关键字实现返回值为void的异步方法,

即不需要对该异步方法进一步交互,称之为:调用并忘记(fire and forget),

许多时候异步编程就是需要这样呀!只是现在我们一般都不使用APM模式罢了!



3.原始线程怎么知道新线程已经运行完毕

其实在实现异步操作的时候,最重要的一个问题就是,在创建了新线程后,原始线程怎么知道新线程已经运行完毕?主要有三种方法:

  1. 一直等待直到完成(wait-until-done):原始线程在通过创建新线程实现异步之后,就自行中断,一直等待,直到异步方法完成在继续。

    在这里就是调用BeginInvoke()后,创建一个新线程后继续执行主线程,但是遇到EndInvoke ()后,主线程则停下来等待新线程的运行结果,直到出结果。

    这种模式,意义不大,你想一想我们为什么要使用异步编程?创建的线程还是要让调用线程等待,违背了我们异步编程的初衷!

  2. 轮询模式(polling):调用线程(即原始线程)定期检查,新线程是否完成,如果没有完成则继续做一些其他的任务。

    在异步委托中,使用AsyncResult类型的对象的IsCompleted属性判断是否完成异步操作,所以通常使用一个while循环来操作

    《精通C#》中是有这样一个比喻“就像项目经理,不停的来问你:‘你完成了吗?’”。

    其实我觉得使用while(IAsyncResult.IsCompleted),一旦异步操作结束,就会立刻的打断while循环中的操作,并不方便!

  3. 回调模式(callback):原始线程在创建新的线程之后,无需等待,也不进行检查。当新创建的线程中的引用方法完成之后,该新创建的线程就会调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结果。

    回调模式呢,则是表示在异步任务完成后次线程主动的告诉调用线程,之后运行回调方法,注意:回调方法是运行在次线程中的

    在之前的等待一直到结束模式 以及 轮询模式 中,初始线程继续它自己的控制流程,直到它知道开启的线程已经完成。然后,它获取结果并继续。

    回调模式的不同之处在于,一旦初始线程发起了异步方法,它就自己管自己了,不再考虑同步。当异步方法调用结束之后,系统调用一个用户自定义的方法(即回调方法)来处理结果,并且在该方法中调用委托的EndInvoke方法。这个用户自定义的方法叫做回调方法或回调。

  4. 三种模式图示:以上三种异步方法调用的标准模式,可以参考下图理解(注:图片来源于《C#图解教程》P431)

    异步方法调用的标准模式



4.使用AsyncCallback委托实现回调模式

在上面,说了那么多,最实际,且最常用的就是回调模式,那么下面就去实现回调模式

实现回调模式,需要使用BeginInvoke的参数列表中最后的两个额外参数,你可记得在之前的示例中我直接使用null作为最后两个参数,这里就具体的看看这两个参数:

  • 倒数第二个是AsyncCallback委托类型的参数,就是用于定义回调方法(若没有回调方法,则可写为null)。

    回调方法的签名和返回类型必须和AsyncCallback委托类型所描述的形式一致。这个委托对象只有一个IAsyncResult类型的参数,返回类型是void,如下所示:

    void AsyncCallback(IAsyncResult iar)

    在回调方法内,我们的代码应该调用委托的EndInvoke方法来处理异步方法执行后的输出值。

  • 倒数第一个参数是Object类型的参数,用于从主线程中传递一个参数进入回调方法(本质上:实现了从主线程中向次线程中传递数据),如果不需要这样一个参数则可以写为null

    因为这个参数类型是System.object,所以可以传入任何回调方法所希望的类型的数据

    这个参数是传入回调方法中,在回调方法中我们可以通过使用IAsyncResult参数的AsyncState属性来获取这个对象,注意获取的是Object类型的对象,需要我们自己强转为其真实类型。

示例:

static void Main(string[] args)
{
    Func<int, int, int> operateAdd = (int num1, int num2) =>
    {
        Thread.Sleep(3000);
         return num1 + num2;
    };
    Console.WriteLine($"当前执行的线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:DoSomethingBeforeAsync...");

    AsyncCallback addCallBack = (IAsyncResult ia) =>
    {
        AsyncResult ar = (AsyncResult)ia;
        int result = ((Func<int, int, int>)ar.AsyncDelegate).EndInvoke(ia);
        Console.WriteLine($"当前执行的新线程,线程ID:{Thread.CurrentThread.ManagedThreadId},异步操作的结果:{result}");
        string state = (string)ia.AsyncState;//使用IAsyncResult对象的AsyncState属性获取BeginInvoke的最后一个参数
        Console.WriteLine($"当前执行的新线程,线程ID:{Thread.CurrentThread.ManagedThreadId},BeginInvoke的最后一个参数:{state}");//state这里是“shanzm”
    };

    IAsyncResult iar = operateAdd.BeginInvoke(1, 2, addCallBack, "shanzm");

    for (int i = 0; i < 6; i++)
    {
        Thread.Sleep(1000);
         Console.WriteLine($"当前执行主线程,线程ID:{Thread.CurrentThread.ManagedThreadId}:...");
    }

    Console.ReadKey();
}

运行结果:
异步委托-回调模式-最后一个参数

说明1:上面的程序中,回调方法我直接使用匿名函数(Lambda表达式)赋值给了AsyncCallBack委托对象,其实可以直接把这个匿名函数写在BeginInvoke() 的参数列表中,但是看上去不优雅!

说明2:使用BeginInvoke()的最后一个参数,传入回调方法,这个参数是Object类型,所以可以传入任何类型的数据,在回调方法中需要强转为真实类型。

至此 ,.NET 异步编程中之AMP模式【完】



5.源代码下载

点击下载源代码

posted @ 2020-02-11 19:01  shanzm  阅读(983)  评论(0编辑  收藏  举报
TOP