c# async/await 大略
这篇文章不在于带领大家深入的探讨async/await编译器内部如何实现的细节,而是从整体上力图告诉大家如何正确的理解它,使用它,克服对它的心理障碍,让使用异步编程的朋友们能跟上时代的步伐,跟上新思路新设计 ,有了这些大概的理解以后不妨碍你在网上再找其它相关那些技术细节实现的介绍文章,这方面网上资源还是不少,好吧让我们先从四个判断说起。
第一个判断 ,yield,await,隐藏了编译器生成的代码,编译器生成的代码是一个被称为,状态机的东西。
第二个判断,它和yield 那个类似,都是有一个MoveNext方法,并且让每个await切分代码段,也象 yield切分块一样,执行,不同的是,一个是在同步中执行,一个是在线程池上执行。
比如:
IEnumerable<int> YieldMethod() { Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); yield return 0; Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); //.. yield return 1; yield return 250; Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个yield之间任意执行的语句,语句数据不限"); yield return 250; //.. } async Task AwaitMethod() {// 这里比yield方法多加了个async Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); await Task.FromResult(1); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); await Task.CompletedTask; Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); await Task.FromResult(3); await Task.FromResult(250); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); Console.WriteLine("表示可以在每个await之间任意执行的语句,语句数据不限"); await Task.FromResult(250 * 2); //... }
这两个方法在编译器后台的大致逻辑类似,都被称为一个状态机,都是对标记返回IEnumeralbe<T>和async(包含await)的的方法进行编译器重构。yield联合foreach语句的调用时由foreach遍历器中的语句交互执行,await直接就是方法中的切断标记。状态机代码由编译器产生,都包含最主要的方法就是MoveNext方法,可以使用反编译工具进行查看,有多少yield,await就会MoveNext多少次,每次调用时,进入不同的switch块分割的块中(当块数大于3时,一般小于等于2时,编译器生成的是if-else块)。
第三个判断,awiat块之间在一个后台线程池上运行,并非某一个线程,这个线程池由Task对象进行管理与代码开发者隔离,无感知对于线程池上的各种调度,跨线程的上下文(如果涉及)等全部代劳。
第四个判断 和yield类似 await的状态机确保了 每个await语句块的在托管线程池所表示 的异步调用中的次序,而与调用线程来说它们是彼此异步的,但对于await所在的异步调度里所有await语句被这个状态机确保其执行次序与用户使用await语句时的编辑次序完全 一致。
对于await来说,使用await意味着使用Task,而Task就是对委托的包装,委托就是异步调用 的基本单元,还记得.net 2.0时期的情况吗?那时的多线程就是使用的委托来做的BeginXXX EndXXX Compled 这个套路但就是因为回调多了嫌弃括号嵌套太深所以就搞了这个async await 没想到很成功,现在已经被各种语言学了去(比如 js),算是在it界普及开了。 我们一定要认识到Task就是委托的一个轻量级包装,并且封装了对线程池的管理,就是System.Threading.ThreadPool这个 2.0时代有的对象 不知是1.1还是2.0可能是.net framework1.1吧,所以一提到Task你就应该立马反应过来,这是一个委托的包装,使用await的方法必须被标记为 async,反过来不一定,只是警告,不会报错,可以执行。async/await的方法,只能由另一个async方法来调用 ,可以形成任意的异步链,就是所谓的async传染性,所以这由引出一个新问题就是,如何 对async/await方法进行消费,那么 一个最正确的方式是 对于有返回值 的直接 用 .Result, 属性,对于没有返回值 的,直接调用 Wait()方法 即可 ,也就是说在你任何一个不想被”传染“的方法里只要把 那个对async方法或对某个Task的调用 前需要加的await 关键字去掉,并且 在这个方法后面根据它是否有返回值 来做就可以了。最后说到返回值 这里又引出一个问题,那就是在async方法里 返回Task, void 差不多,等于没有返回值 ,返回 async Task<T> 就等于返回 T 就好象说 async Task互相抵消了一下, 而让它们抵销的正是 await 来实现 的 如果 你把一个async Task<T>的返回方法 中的async去掉,那么方法体中await就会报错,你再把await也去掉会怎么样,这时你发现你的返回值 不再是原本要求 的T了而是Task<T> 了 从这个细节,可以猜测async/await是帮助把Task 的效果或影响给消除了,让用户好象只用关心Task<T>中的T一样不用在操心 Task了,而Task正是代表了,异步调用 和对多线程的封装,从而达到让开发者,更容易使用多线程的目的。