C# 彻底搞懂async/await
前言
talk is cheap, show you the code first!
private void button1_click(object sender, eventargs e) { console.writeline("111 balabala. my thread id is :" + thread.currentthread.managedthreadid); asyncmethod(); console.writeline("222 balabala. my thread id is :" + thread.currentthread.managedthreadid); } private async task asyncmethod() { var resultfromtimeconsumingmethod = timeconsumingmethod(); string result = await resultfromtimeconsumingmethod + " + asyncmethod. my thread id is :" + thread.currentthread.managedthreadid; console.writeline(result); //返回值是task的函数可以不用return } //这个函数就是一个耗时函数,可能是io操作,也可能是cpu密集型工作。 private task<string> timeconsumingmethod() { var task = task.run(()=> { console.writeline("helo i am timeconsumingmethod. my thread id is :" + thread.currentthread.managedthreadid); thread.sleep(5000); console.writeline("helo i am timeconsumingmethod after sleep(5000). my thread id is :" + thread.currentthread.managedthreadid); return "hello i am timeconsumingmethod"; }); return task; }
我靠,这么复杂!!!竟然有三个函数!!!竟然有那么多行!!!
别着急,慢慢看完,最后的时候你会发现使用async/await
真的炒鸡优雅。
异步方法的结构
上面是一个的使用async/await
的例子(为了方便解说原理我才写的这样复杂的)。
使用async/await
能非常简单的创建异步方法,防止耗时操作阻塞当前线程。
使用async/await
来构建的异步方法,逻辑上主要有下面三个结构:
调用异步方法
异步方法的返回类型只能是void
、task
、task<tresult>
。示例中异步方法的返回值类型是task
。
另外,上面的asyncmethod()会被编译器提示报警,如下图:
因为是异步方法,所以编译器提示在前面使用await
关键字,这个后面再说,为了不引入太多概念导致难以理解暂时就先这么放着。
异步方法本体
被await修饰的只能是task
或者task<tresule>
类型,通常情况下是一个返回类型是task/task<tresult>
的方法,当然也可以修饰一个task/task<tresult>
变量,await只能出现在已经用async关键字修饰的异步方法中。上面代码中就是修饰了一个变量resultfromtimeconsumingmethod
。
关于被修饰的对象,也就是返回值类型是task
和task<tresult>
函数或者task/task<tresult>
类型的变量:如果是被修饰对象的前面用await
修饰,那么返回值实际上是void
或者tresult
(示例中resultfromtimeconsumingmethod
是timeconsumingmethod()
函数的返回值,也就是task<string>
类型,当resultfromtimeconsumingmethod
在前面加了await
关键字后 await resultfromtimeconsumingmethod
实际上完全等于 resultfromtimeconsumingmethod.result
)。如果没有await
,返回值就是task
或者task<tresult>
。
耗时函数
在示例中是一个cpu密集型的工作,我另开一线程让他拼命干活干5s。如果是io密集型工作比如文件读写等可以直接调用.net提供的类库,对于这些类库底层具体怎么实现的?是用了多线程还是dma?或者是多线程+dma?这些问题我没有深究但是从表象看起来和我用task另开一个线程去做耗时工作是一样的。
await
只能修饰task/task<tresult>
类型,所以这个耗时函数的返回类型只能是task/task<tresult>
类型。
总结:有了上面三个结构就能完成使用一次异步函数。
async/await异步函数的原理
在开始讲解这两个关键字之前,为了方便,对某些方法做了一些拆解,拆解后的代码块用代号指定:
上图对示例代码做了一些指定具体就是:
- caller代表调用方函数,在上面的代码中就是button1_click函数。
- calleeasync代表被调用函数,因为代码中被调用函数是一个异步函数,按照微软建议的命名添加了async后缀,在上面示例代码中就是asyncmethod()函数。
- callerchild1代表调用方函数button1_click在调用异步方法calleeasync之前的那部分代码。
- callerchild2代表调用方函数button1_click在调用异步方法calleeasync之后的那部分代码。
- calleechild1代表被调用方函数asyncmethod遇到await关键字之前的那部分代码。
- calleechild2代表被调用方函数asyncmethod遇到await关键字之后的那部分代码。
- timeconsumingmethod是指被await修饰的那部分耗时代码(实际上我代码中也是用的这个名字来命名的函数)
示例代码的执行流程
为了方便观看我模糊掉了对本示例没有用的输出。
这里涉及到了两个线程,线程id分别是1和3。
caller函数被调用,先执行callerchild1代码,这里是同步执行与一般函数一样,然后遇到了异步函数calleeasync。
在calleeasync函数中有await关键字,await的作用是打分裂点。
编译器会把整个函数(calleeasync)从这里分裂成两个函数。await关键字之前的代码作为一个函数(按照我上面定义的指代,下文中就叫这部分代码calleechild1)await关键字之后的代码作为一个函数(calleechild2)。
calleechild1在调用方线程执行(在示例中就是主线程thread1),执行到await关键字之后,另开一个线程耗时工作在thread3中执行,然后立即返回。这时调用方会继续执行下面的代码callerchild2(注意是caller不是callee)。
在callerchild2被执行期间,timeconsumingmethod也在异步执行(可能是在别的线程也可能是cpu不参与操作直接dma的io操作)。
当timeconsumingmethod执行结束后,calleechild2也就具备了执行条件,而这个时候callerchild2可能执行完了也可能没有,由于callerchild2与calleechild2都会在caller的线程执行,这里就会有冲突应该先执行谁,编译器会在合适的时候在caller的线程执行这部分代码。示意图如下:
请注意,calleechild2在上图中并没有画任何箭头,因为这部分代码的执行是由编译器决定的,暂时无法具体描述是什么时候执行。
总结一下:
整个流程下来,除了timeconsumingmethod函数是在thread3中执行的,剩余代码都是在主线程thread1中执行的.
也就是说异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间,异步模型采用时间片轮转来实现(这一点我没考证,仅作参考)。
你也许会说,明明新加了一个thread3线程怎么能说是运行在当前的线程中呢?这里说的异步方法运行在当前线程上的意思是由calleeasync分裂出来的calleechild1和calleechild2的确是运行在thread1上的。
带返回值的异步函数
之前的示例代码中异步函数是没有返回值的,作为理解原理足够了,但是在实际应用场景中,带返回值的应用才是最常用的。那么,上代码:
按理说没错吧?然而,这代码一旦执行就会卡死。
死锁
是的,死锁。分析一下为什么:
按照之前我划定的代码块指定,在添加了新代码后callerchild2与calleechild2的划分如上图。
这两部分代码块都是在同一个线程上执行的,也就是主线程thread1,而且通常情况下callerchild2是会早于calleechild2执行的(毕竟calleechild2得在耗时代码块执行之后执行)。
console.writeline(resulttask.result);
(callerchild2)其实是在请求calleechild2的执行结果,此时明显calleechild2还没有结束没有return任何结果,那console.writeline(resulttask.result);
就只能阻塞thread1等待,直到calleechild2有结果。
然而问题就在这,calleechild2也是在thread1上执行的,此时callerchild2一直占用thread1等待calleechild2的结果,耗时程序结束后轮到calleechild2执行的时候calleechild2又因thread1被callerchild2占用而抢不到线程,永远无法return,那么callerchild2就会永远等下去,这就造成了死锁。
解决办法有两种一个是把console.writeline(resulttask.result);
放到一个新开线程中等待(个人觉得这方法有点麻烦,毕竟要新开线程),还有一个方法是把caller也做成异步方法:
resulttask.result变成了resulttask 的原因上面也说了,await修饰的task/task<tresult>
得到的是tresult。
之所以这样就能解决问题是因为嵌套了两个异步方法,现在的caller也成了一个异步方法,当caller执行到await后直接返回了(await拆分方法成两部分),calleechild2执行之后才轮到caller中await后面的代码块(console.writeline(resulttask.result);
)。
另外,把caller做成异步的方法也解决了一开始的那个警告,还记得么?
这样没省多少事啊?
到现在,你可能会说:使用async/await
不比直接用task.run()来的简单啊?比如我用task
的taskcontinuewith
方法也能实现:
参考:
死锁问题 https://www.cnblogs.com/opencoder/p/4434574.html
该博主是翻译的英文资料,英文原文:
https://www.cnblogs.com/zhili/archive/2013/05/15/csharp5asyncandawait.html
上一篇: 南海三成五金企业使用机器人 实现智能化