.NET异步编程之APM模式
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.原始线程怎么知道新线程已经运行完毕
其实在实现异步操作的时候,最重要的一个问题就是,在创建了新线程后,原始线程怎么知道新线程已经运行完毕?主要有三种方法:
一直等待直到完成(wait-until-done):原始线程在通过创建新线程实现异步之后,就自行中断,一直等待,直到异步方法完成在继续。
在这里就是调用BeginInvoke()后,创建一个新线程后继续执行主线程,但是遇到EndInvoke ()后,主线程则停下来等待新线程的运行结果,直到出结果。
这种模式,意义不大,你想一想我们为什么要使用异步编程?创建的线程还是要让调用线程等待,违背了我们异步编程的初衷!
轮询模式(polling):调用线程(即原始线程)定期检查,新线程是否完成,如果没有完成则继续做一些其他的任务。
在异步委托中,使用AsyncResult类型的对象的IsCompleted属性判断是否完成异步操作,所以通常使用一个while循环来操作
《精通C#》中是有这样一个比喻“就像项目经理,不停的来问你:‘你完成了吗?’”。
其实我觉得使用while(IAsyncResult.IsCompleted),一旦异步操作结束,就会立刻的打断while循环中的操作,并不方便!
回调模式(callback):原始线程在创建新的线程之后,无需等待,也不进行检查。当新创建的线程中的引用方法完成之后,该新创建的线程就会调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结果。
回调模式呢,则是表示在异步任务完成后次线程主动的告诉调用线程,之后运行回调方法,注意:回调方法是运行在次线程中的。
在之前的
等待一直到结束模式
以及轮询模式
中,初始线程继续它自己的控制流程,直到它知道开启的线程已经完成。然后,它获取结果并继续。回调模式的不同之处在于,一旦初始线程发起了异步方法,它就自己管自己了,不再考虑同步。当异步方法调用结束之后,系统调用一个用户自定义的方法(即回调方法)来处理结果,并且在该方法中调用委托的EndInvoke方法。这个用户自定义的方法叫做回调方法或回调。
三种模式图示:以上三种异步方法调用的标准模式,可以参考下图理解(注:图片来源于《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模式的更多相关文章
- 异步编程之APM
一.APM概述 APM即异步编程模型的简写(Asynchronous Programming Model),我们平时经常会遇到类似BeginXXX和EndXXX的方法,我们在使用这些方法的时候,其实就 ...
- 异步编程之Generator(1)——领略魅力
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
- 异步编程之Promise(3):拓展进阶
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
- 异步编程之Promise(2):探究原理
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
- 异步编程之Generator(2)——剖析特性
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
- (翻译)异步编程之Promise(1):初见魅力
原文:https://www.promisejs.org/ by Forbes Lindesay 异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2) ...
- net异步编程之await
net异步编程之await 初探asp.net异步编程之await 终于毕业了,也顺利进入一家期望的旅游互联网公司.27号入职.放肆了一个多月没写代码,好方啊. 另外一下观点均主要针对于await ...
- Javascript异步编程之setTimeout与setInterval详解分析(一)
Javascript异步编程之setTimeout与setInterval 在谈到异步编程时,本人最主要会从以下三个方面来总结异步编程(注意:特别解释:是总结,本人也是菜鸟,所以总结不好的,请各位大牛 ...
- 异步编程之co——源码分析
异步编程系列教程: (翻译)异步编程之Promise(1)--初见魅力 异步编程之Promise(2):探究原理 异步编程之Promise(3):拓展进阶 异步编程之Generator(1)--领略魅 ...
随机推荐
- 「洛谷P3469」[POI2008]BLO-Blockade 解题报告
P3469[POI2008]LO-Blockade 题意翻译 在Byteotia有n个城镇. 一些城镇之间由无向边连接. 在城镇外没有十字路口,尽管可能有桥,隧道或者高架公路(反正不考虑这些).每两个 ...
- 在Git的PR(Pull Request)提示冲突无法merge合并的解决方案
问题 假设有一个分支A,向master分支提交PR,然后发生无法自动解决的冲突,PR提示不能执行merge合并. 解决方案1 本地checkout检出并切换到A分支,pull拉取更新到最新代码 在本地 ...
- centos7下图形界面和命令行界面切换
在图形界面使用 ctrl+alt+F2切换到dos界面 dos界面 ctrl+alt+F2切换回图形界面 在命令上 输入 init 3 命令 切换到dos界面 输入 init 5命令 切换到图形界面 ...
- 关于MySQL幻读的实验
该实验基于 CentOS 7 + MySQL 5.7 进行 打开两个窗口连接到MySQL 第一个连接的事务我们命名为 T1 第二个连接的事务我们命名为 T2 T2 发生在 T1 的 O1 操作结束以 ...
- Match3 Module For Game(THDN)
介绍 THDN的核心机制为Match3的利用,本文对Match3 Gameplay进行记录,并对其进行改良.THDN作为RogueLIke性质的游戏,玩家在随机生成的dungeon里进行探索并获 ...
- Android通过子线程更新UI的几种方式
一般情况下,UI的更新都少不了Handler,首先我们先了解一下Handler机制: Handler消息机制 定义 Message 线程间通信的数据单元,可通过message携带需要的数据创建对象:M ...
- Scala实践14
1.Scala的future 创建future import scala.concurrent._ import ExecutionContext.Implicits.global object Fu ...
- USACO简介导论
1000: USACO简介 时间限制: 1 Sec 内存限制: 128 MB提交: 8 解决: 7[提交] [状态] [讨论版] [命题人:外部导入] 题目描述 来源/分类 USACO-00 ...
- 修理牛棚 贪心 USACO
今天开始终于可以刷USACO的题啦 准备每一道都发一个题解 1010: 1.3.2 Barn Repair 修理牛棚 时间限制: 1 Sec 内存限制: 128 MB提交: 9 解决: 7[提交] ...
- 【C&数据结构】---关于链表结构的前序插入和后序插入
刷LeetCode题目,需要用到链表的知识,忽然发现自己对于链表的插入已经忘得差不多了,以前总觉得理解了记住了,但是发现真的好记性不如烂笔头,每一次得学习没有总结输出,基本等于没有学习.连复盘得机会都 ...