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.源代码下载

点击下载源代码

.NET异步编程之APM模式的更多相关文章

  1. 异步编程之APM

    一.APM概述 APM即异步编程模型的简写(Asynchronous Programming Model),我们平时经常会遇到类似BeginXXX和EndXXX的方法,我们在使用这些方法的时候,其实就 ...

  2. 异步编程之Generator(1)——领略魅力

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  3. 异步编程之Promise(3):拓展进阶

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  4. 异步编程之Promise(2):探究原理

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  5. 异步编程之Generator(2)——剖析特性

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

  6. (翻译)异步编程之Promise(1):初见魅力

    原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...

  7. net异步编程之await

    net异步编程之await 初探asp.net异步编程之await   终于毕业了,也顺利进入一家期望的旅游互联网公司.27号入职.放肆了一个多月没写代码,好方啊. 另外一下观点均主要针对于await ...

  8. Javascript异步编程之setTimeout与setInterval详解分析(一)

    Javascript异步编程之setTimeout与setInterval 在谈到异步编程时,本人最主要会从以下三个方面来总结异步编程(注意:特别解释:是总结,本人也是菜鸟,所以总结不好的,请各位大牛 ...

  9. 异步编程之co——源码分析

    异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...

随机推荐

  1. 洛谷$P2053\ [SCOI2007]$修车 网络流

    正解:网络流 解题报告: 传送门$QwQ$ 一个很妙的建图,,,说实话我麻油想到$QwQ$ 考虑对每个工人建$n$个点,表示这是他修的倒数第$i$辆车,就可以算出影响是$t\cdot i$,然后对每辆 ...

  2. $Noip2011/Luogu1311$ 选择客栈

    $Luogu$ $Sol$ 暴力十分显然叭.正解不是很好想. 我最开始想维护所有色调的客栈的前缀和后缀,然后每扫到一个最低消费合法的就统计一次答案.但是这样会重复计数,两个合法客栈之间有几个消费合法的 ...

  3. must appear in the GROUP BY clause or be used in an aggregate function

    今天在分组统计的时候pgsql报错 must appear in the GROUP BY clause or be used in an aggregate function,在mysql里面是可以 ...

  4. extract函数的使用

    EXTRACT(field FROM source) extract函数从日期/时间数值里抽取子域,比如年.小时等. source必须是一个timestamp, time, interval类型的值表 ...

  5. Objectarx 相交矩形求并集 面域转多段线

    测试结果: 主要思路:拾取一个点作为矩形的插入点,分别以该点进行两次jig操作,就能得到白色的两个相交的polyline,之后需要变成红色的封闭多段线.做法就是:求出两个白色矩形的面域,然后通过boo ...

  6. App的基本结构

    今天主要学习安装了Android Studio,并且成功地在虚拟机上运行了HelloWord工程.下面针对HelloWord工程对app的基本框架结构进行一个总结.掌握app的基本结构对初学软件开发的 ...

  7. 官方文档中文版!Spring Cloud Stream 快速入门

    本文内容翻译自官方文档,spring-cloud-stream docs,对 Spring Cloud Stream的应用入门介绍. 一.Spring Cloud Stream 简介 官方定义 Spr ...

  8. pycharm 安装vue

    1.设置JS为ES6 2.安装vue.js 3.重启pycharm 4.检查

  9. Django admin的常用方法

    一.HTTP 1.主页面 http://127.0.0.1:8000/admin/ 2.查询页面 http://127.0.0.1:8000/admin/app01/book/ 3.增加页面 http ...

  10. Java入门 - 语言基础 - 03.基础语法

    原文地址:http://www.work100.net/training/java-basic-syntax.html 更多教程:光束云 - 免费课程 基础语法 序号 文内章节 视频 1 第一个Jav ...