zoukankan      html  css  js  c++  java
  • .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来替代!)。
    使用起来有些坑,不详细写于此了,可以看点击:示例



    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)
    {
        AddAsyncWithCallBack2();
        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.源代码下载

    点击下载源代码

  • 相关阅读:
    Android学习笔记ActionView
    Android学习笔记添加ActionItem
    Android学习笔记显示和隐藏ActionBar
    Android学习笔记上下文菜单
    Android学习笔记菜单资源文件
    Android学习笔记样式资源文件
    Android学习笔记主题(Theme)资源文件
    Android学习笔记StateListDrawable文件
    Android学习笔记.9.png格式图片
    笔记本外接显示器设置全屏壁纸
  • 原文地址:https://www.cnblogs.com/shanzhiming/p/12296283.html
Copyright © 2011-2022 走看看