C#复习笔记(5)--C#5:简化的异步编程(异步编程的深入分析)
首先,阐明一下标题的这个“深入分析”起得很惭愧,但是又不知道该起什么名字,这个系列也主要是做一些复习的笔记,供自己以后查阅,如果能够帮助到别人,那自然是再好不过了。
然后,我想说的是异步方法的状态机真的是太复杂了。我写完这篇都还迷迷糊糊的,所以,读者就不要往下看了。这里面还涉及大量的核心类型没有搞清楚。
异步编程的深入分析
先来一下整体的概括:异步编程的实现(包括近似实现和真实编译器生成的代码)基本上可以说是一个状态机。编译器将生成一个私有的内嵌结构(现在是类了),来表示这个异步方法。编译器同时“改造了”那个异步方法,其签名与所声明的方法签名相同。我称其为骨架方法,该方法本身没有多少内容,但其他东西都依赖于它。骨架方法需要创建状态机,并执行一个步骤(此处的步骤指执行第一个await表达式之前的代码),然后返回一个表示状态机进度的任务。(别忘了,在第一次到达真正需要等待的await表达式之前,执行过程是同步的。)此后,骨架方法的运作就此结束。状态机会负责其余事项,后续操作附加到其他异步操作后,可通知状态机去执行另一个步骤。当之前返回的任务被赋予适当的值后,方法就执行到最后了,状态机可随即发出信号。下图展示了这一流程图。

当然,“执行方法体中的代码”这一步,只有在骨架方法中第一次调用时,才会从方法的开头执行。以后每次到达该块,都是由后续操作从之前中断的地方开始继续执行。
这个状态机表示为一个密封类(或者结构,我用ILSpy反编译后看到的是一个类),在了解细节之前,先看看编译器生成的这个状态机到底是什么样子,首先看一下“源码”示例:
static async Task<int> SumCharactersAsync(IEnumerable<char> text)
{
int total = ;
foreach (char ch in text)
{
int unicode = ch;
await Task.Delay(unicode);
total += unicode;
}
await Task.Yield();
return total;
}
在编译器看到async和await关键字之后,就会在后台生成一个状态机类,这个类实现一个IAsyncStateMachine接口,这个接口定义如下:
public interface IAsyncStateMachine
{
void MoveNext(); void SetStateMachine(IAsyncStateMachine stateMachine);
}
状态机主要逻辑都在MoveNext里面。
在查看生成的状态机之前,先指出以下几点:
- 该方法包含一个参数(text)。
- 该方法包含一个循环,后续操作执行时需跳回该循环内。
- 该方法包含两个不同类型的await表达式:Task.Delay返回一个Task,而Task.Yield()则返回一个YieldAwaitable。而awaitable模式最重要的实现是拥有一个GetAwaiter的方法。该方法返回一个awaiter,awaiter实现ICriticalNotifyCompletion, INotifyCompletion这两个接口的核心是上下文。这两个接口中的UnsafeOnCompleted方法和OnCompleted方法最终会在AsyncTaskMethodBuilder(如果有返回值话,它是一个泛型的类)里面的AwaitUnsafeOnCompleted方法或者AwaitOnCompleted方法里面进行调用。AwaitUnsafeOnCompleted方法或者AwaitOnCompleted方法中会对ExecutionContext进行封送,ExecutinContext是所有上下文的容器,比如SynchronizationContext。ExecutionContext的Capture方法和Run方法是在附加后续操作时进行捕获上下文和执行后续操作时还原上下文的关键。这两个方法会在awaiter和AsyncTaskMethodBuilder(及其兄弟类)进行调用。
- 该方法包含显式的局部变量(total、ch和unicode),需在不同的调用间关注其变化。
- 该方法包含一个通过调用text.GetEnumerator()方法创建的隐式局部变量。
- 该方法最终返回一个值。
上面的这个“源码”会被编译器改造成骨架方法
先来看一下这个骨架方法:

可以看到骨架方法中首先声明了一个状态机:
然后对这个状态机中的字段做了一些初始化的工作。可以看到这个状态机的名字就是用源码中方法的名字加上一些特定的字符后组成的。然后初始化了一些字段,最后,
这句代码让状态机同步地执行第一个步骤,并在方法完成时或到达需等待的异步操作点时得以返回。第一个步骤完成后,骨架方法将返回builder中的任务。状态机在结束时,会使用builder来设置结果或异常。
我们得看一下生成的这个状态机了:

状态机一般是由两大部分组成:
第一部分是字段:我将字段分成了三种类型,为了方便,我在字段的后面用小括号加注释加以说明:
- ①固定的字段,在本例中,就是<>t_builder(异步方法生成器,AsyncTaskMethodBuilder),<>u_1(awaiter),还有一个表示状态机状态的<>1_state字段。对于<>t_builder(异步方法生成器)来说,不同的返回类型的异步方法被编译器处理后有不同类型结构:如果返回void,那么<>t_builder(异步方法生成器)就是AsyncVoidMethodBuilder类型的,如果返回一个Task<T>,那么就是AsyncTaskMethodBuilder<T>,如果返回Task,就是AsyncTaskMethodBuilder;<>t_builder(异步方法生成器)具有很多功能,包括创建骨架方法返回的Task和Task<T>,即异步方法结束时传播的任务,其内包含有正确结果。对于<>u_1(awaiter)来说,是由await表达式中的可等待类型来决定的,本例中,异步方法有两个await表达式,他们的类型都不一样,一个是TaskAwaiter,另一个是YieldAwaitable.YieldAwaiter。异步方法中使用的awaiter如果是值类型,则每个类型都会有一个字段与之对应,而如果是引用类型(编译时的类型),则所有awaiter共享一个字段。本例有两个await表达式,分别使用两个不同的awaiter结构类型,因此有两个字段。如果第二个await表达式也使用了一个TaskAwaiter,或者如果TaskAwiater和YieldAwiter都是类,则只会有一个字段。由于一次只能存活一个awaiter,因此即使一次只能存储一个值也没关系。
- ②由方法传入的参数被提升的字段,如果异步方法有参数,那么在生成的状态机中会将这些参数提升成状态机的字段,在这里是text。
- ③方法中使用的局部变量。此例中涉及到的局部变量有total、unicode,还有一个foreach循环中调用GetEnumerator生成的局部变量。将局部变量提升为字段是由于需在多次调用MoveNext()方法时保存变量的值。
第二部分是方法:方法都是实现了IStateMachine接口中的,一个是MoveNext,另一个是SetStateMachine。
MoveNext方法在一开始便投入使用,并且可用于所有await表达式的后续操作。每当调用MoveNext()方法时,状态机就会通过state字段计算出方法要跳转到的位置。在准备计算结果时,则跳转到方法的逻辑起始位置或await表达式的末尾。每个状态机只执行一次操作。实际上,在方法内部存在一个基于state的switch语句,每种情况都具有包含不同标签的对应goto语句。
本例中的MoveNext方法的代码如下:


可以看到这个方法很长,看着很恶心。
初始状态始终为-1,方法执行时状态也是-1(与等待时被暂停相反)。非负值均表示一个后续操作的目标。状态机在结束时的状态为-2。
在方法执行过程中,在原始异步方法的return语句处,会设置result变量。然而在到达方法的逻辑末尾时,将其用于builder.SetResult()的调用。即使是非泛型的AysncTaskMethodBuilder和AsyncVoidMethodBuilder类型,也包含SetResult()方法。前者表示对于从骨架方法返回的任务来说,该方法已经完成;后者则表示原始的SynchronizationContext已经完成。(异常会以同样的方式向原始的SynchronizationContext传播。这是一种相当丑陋的跟踪方式,但却对必须使用void方法的场景提供了一种解决方案。)
任何await表达式均表示执行路径的一个分支。首先,被等待的异步操作得到一个awaiter,然后检查其IsCompleted属性。若返回true,即可立即获得结果并继续。否则,需进行以下处理。
- 存储awaiter,以供后面使用。
- 更新状态,以表示从哪里继续。
- 为awaiter附加后续操作。
- 从MoveNext()返回,确保不会执行任何finally块。

如果等待(await)的操作有返回值(如使用HttpClient分配awaitclient.GetStringAsync(…)的结果),那么上述代码末尾处的GetResult()调用将得到该值。类似于
的代码。
AwaitUnsafeOnCompleted方法将后续操作附加给awaiter,MoveNext()方法开头的switch语句可确保再次执行MoveNext()时,将控制传递给DemoAwaitContinuation。
说明 AwaitOnCompleted和AwaitUnsafeOnCompleted 在此前展示的一组接口中,IAwaiter<T>扩展了INotifyCompletion及其OnCompleted方法,此外还扩展了ICriticalNotifyCompletion接口及其UnsafeOnCompleted方法。状态机为实现ICriticalNotifyCompletion的awaiter调用builder.AwaitUnsafeOnCompleted,或为只实现INotifyCompletion的awaiter调用builder.AwaitOnCompleted。后续的章节在讨论可等待模式如何与上下文交互时,会介绍这两个调用间的区别。
C#复习笔记(5)--C#5:简化的异步编程(异步编程的深入分析)的更多相关文章
- Java基础复习笔记系列 九 网络编程
Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...
- Java基础复习笔记系列 八 多线程编程
Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...
- Java基础复习笔记系列 七 IO操作
Java基础复习笔记系列之 IO操作 我们说的出入,都是站在程序的角度来说的.FileInputStream是读入数据.?????? 1.流是什么东西? 这章的理解的关键是:形象思维.一个管道插入了一 ...
- Java基础复习笔记系列 五 常用类
Java基础复习笔记系列之 常用类 1.String类介绍. 首先看类所属的包:java.lang.String类. 再看它的构造方法: 2. String s1 = “hello”: String ...
- Java基础复习笔记系列 四 数组
Java基础复习笔记系列之 数组 1.数组初步介绍? Java中的数组是引用类型,不可以直接分配在栈上.不同于C(在Java中,除了基础数据类型外,所有的类型都是引用类型.) Java中的数组在申明时 ...
- Java基础复习笔记基本排序算法
Java基础复习笔记基本排序算法 1. 排序 排序是一个历来都是很多算法家热衷的领域,到现在还有很多数学家兼计算机专家还在研究.而排序是计算机程序开发中常用的一种操作.为何需要排序呢.我们在所有的系统 ...
- 机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据
机器学习实战(Machine Learning in Action)学习笔记————09.利用PCA简化数据 关键字:PCA.主成分分析.降维作者:米仓山下时间:2018-11-15机器学习实战(Ma ...
- Angular复习笔记7-路由(下)
Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...
- Angular复习笔记7-路由(上)
Angular复习笔记7-路由(上) 关于Angular路由的部分将分为上下两篇来介绍.这是第一篇. 概述 路由所要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表 ...
- Angular复习笔记6-依赖注入
Angular复习笔记6-依赖注入 依赖注入(DependencyInjection)是Angular实现重要功能的一种设计模式.一个大型应用的开发通常会涉及很多组件和服务,这些组件和服务之间有着错综 ...
随机推荐
- 使用Java命令行方式导入第三方jar包来运行Java程序的命令
1.首先使用命令行进入到a.java所在的文件夹:(比如我的在D:\javaeeworkspace\SharedPS_WS\src\com\dyf\main 这样一个路径下,) d: 回车, cd D ...
- Announcing the Updated NGINX and NGINX Plus Plug‑In for New Relic (Version 2)
In March, 2013 we released the first version of the “nginx web server” plug‑in for New Relic monitor ...
- A - 畅通工程续 最短路
某省自从实行了很多年的畅通工程计划后,终于修建了很多路.不过路多了也不好,每次要从一个城镇到另一个城镇时,都有许多种道路方案可以选择,而某些方案要比另一些方案行走的距离要短很多.这让行人很困扰. 现在 ...
- 【ZJOI2017】仙人掌
[ZJOI2017]仙人掌 参考博客:https://www.cnblogs.com/wfj2048/p/6636028.html 我们先求出\(dfs\)树(就是\(dfs\)一遍),然后问题就变成 ...
- Pandas 的数据结构
Pandas的数据结构 导入pandas: 三剑客 from pandas import Series,DataFrame import pandas as pd import numpy as np ...
- SQL IN 操作符
IN 操作符 IN 操作符允许我们在 WHERE 子句中规定多个值. SQL IN 语法 SELECT column_name(s) FROM table_name WHERE column_name ...
- SQLite的原子提交--单文件场景
3. 单文件提交 我们首先概要说明SQLite在单个数据库文件上为了执行事务的原子提交而采取的步骤.在后面的部分将讨论如何设计文件格式以保护其在断电故障中损坏,以及原子提交在多个数据库上的执行. 3. ...
- springBoot 搭建web项目(前后端分离,附项目源代码地址)
springBoot 搭建web项目(前后端分离,附项目源代码地址) 概述 该项目包含springBoot-example-ui 和 springBoot-example,分别为前端与后端,前后端 ...
- 转://使用insert插入大量数据的总结
使用insert插入大量数据的个人经验总结在很多时候,我们会需要对一个表进行插入大量的数据,并且希望在尽可能短的时间内完成该工作,这里,和大家分享下我平时在做大量数据insert的一些经验. 前提:在 ...
- json_encode里面经常用到的 JSON_UNESCAPED_UNICODE和JSON_UNESCAPED_SLASHES
php格式化json的函数json_encode($value,$options) 其中有2个比较常用到的参数 JSON_UNESCAPED_UNICODE(中文不转为unicode ,对应的数字 2 ...