第6章 异步原理
6.1 生成代码的结构
异步模式的实现原理是基于 状态机 的,它负责追踪 async 方法当前的执行进度。从逻辑上讲,可以分为以下 4 种状态:
- 未启动
- 正在执行
- 暂停
- 完成(成功或 faulted)
Eureka
这里的“暂停”,指程序运行至 await 处,任务未完成时,当前方法在此处挂起(暂停)。
async 方法中的每个 await 表达式是单独的状态,每次返回后都会触发后续代码的执行。只有当状态机需要进入 暂停 时,才需要记录状态(记录状态旨在从当前执行位置恢复执行)。下图演示了不同状态之间的转换关系:
下面两段代码演示了原代码和编译器转化后的代码(有删改):
static async Task PrintAndWait(TimeSpan delay)
{Console.WriteLine("Before first delay");await Task.Delay(delay);Console.WriteLine("Between delays");await Task.Delay(delay);Console.WriteLine("After second delay");
}
// 桩方法
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
private static unsafe Task PrintAndWait(TimeSpan delay)
{var machine = new PrintAndWaitStateMachine{ //delay = delay, // 初始化状态机,builder = AsyncTaskMethodBuilder.Create(), // 包括方法参数state = -1 //};machine.builder.Start(ref machine); // 运行状态机,直到需要等待为止return machine.builder.Task; // 返回代表异步操作的 task
}// 状态机的私有结构体
[CompilerGenerated]
private struct PrintAndWaitStateMachine : IAsyncStateMachine
{public int state; // 状态机的状态public AsyncTaskMethodBuilder builder; // 异步基础架构类型所关联的 builderprivate TaskAwaiter awaiter; // 恢复执行时用于获取结果的 awaiterpublic TimeSpan delay; // 原始方法参数void IAsyncStateMachine.MoveNext(){// 状态机主要的工作代码,此处已省略}[DebuggerHidden]void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine){// 连接 builder 和 装箱后的状态机this.builder.SetStateMachine(stateMachine);}
}
6.1.1 桩方法:准备和开始第一步
延续前文内容,以如下代码为例,编译器创建的 PrintAndWait()
便是桩方法:
// 桩方法
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
private static unsafe Task PrintAndWait(TimeSpan delay)
{var machine = new PrintAndWaitStateMachine{ //delay = delay, // 初始化状态机,builder = AsyncTaskMethodBuilder.Create(), // 包括方法参数state = -1 //};machine.builder.Start(ref machine); // 运行状态机,直到需要等待为止return machine.builder.Task; // 返回代表异步操作的 task
}
状态机 在桩方法中创建,主要有以下 3 点信息:
- 形参:每个形参在状态机中都是独立的 字段
- builder :该对象随着 async 方法 返回类型 的不同而不同。
该对象始终是 值 类型,桩方法通过它的 Start()
方法启动状态机,并将状态机自身以 引用 方式传入(状态机也是 值 类型,引用传递用于避免值拷贝)
因值类型的特点,如下重构方式不可行:
var builder = machine.builder;
builder.Start(ref machine);
return builder.Task;
- 初始状态:永远是 -1
6.1.2 状态机的结构
延续前文内容,以如下代码为例,编译器创建的 PrintAndWaitStateMachine
结构体便是状态机:
// 状态机的私有结构体
[CompilerGenerated]
private struct PrintAndWaitStateMachine : IAsyncStateMachine
{public int state; // 状态机的状态public AsyncTaskMethodBuilder builder; // 异步基础架构类型所关联的 builderprivate TaskAwaiter awaiter; // 恢复执行时用于获取结果的 awaiterpublic TimeSpan delay; // 原始方法参数void IAsyncStateMachine.MoveNext(){// 状态机主要的工作代码,此处已省略}[DebuggerHidden]void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine){// 连接 builder 和 装箱后的状态机this.builder.SetStateMachine(stateMachine);}
}
该类型有如下特点:
- 它实现了
IAsyncStateMachine
接口,该接口用于异步基础架构。 - 字段由状态机在 步进 (
MoveNext()
方法调用时)时使用。 -
MoveNext()
方法在状态机 启动 后或 暂停恢复 后被调用。 -
SetStateMachine()
方法的实现总是保持不变(在 release 模式下)。
其中的字段大致可分为以下 5 类
-
当前状态(例如未启动、暂停等待某个 await 表达式等);
有以下几种可能值:
- -1:尚未启动或正在执行
- -2:执行完成(成功或 faulted)
- 其他值:正在某个 await 表达式处暂停
-
方法 builder,用于和异步基础架构交互,并且提供返回的 Task;
该对象类型可以是:
-
AsyncVoidMethodBuilder
-
AsyncTaskMethodBuilder
-
AsyncTaskMethodBuilder<T>
- 自定义 task 类型的 builder
-
-
awaiter;
该字段的数量取决于当前异步方法等待几类 Task 类型。以如下代码为例,编译器将创建
TaskAwaiter
、TaskAwaiter<int>
、TaskAwaiter<string>
三个 awaiter,分别用于 获取它们的结果static async Task PrintAndWait(TimeSpan delay) {await Task.Delay(delay);Console.WriteLine("delayed");string value = await GetString();Console.WriteLine(value);int num = await GetNumber();Console.WriteLine(num); }static async Task<int> GetNumber() {await Task.Delay(TimeSpan.FromSeconds(1));return 1; }static async Task<string> GetString() {await Task.Delay(TimeSpan.FromSeconds(1));return "1"; }
-
形参和局部变量;
-
临时栈变量。
当 await 表达式用作其他表达式的一部分,会用到临时栈变量。如下代码便涉及该情况:
public async Task TemporaryStackDemoAsync() {Task<int> task = Task.FromResult(10);DateTime now = DateTime.UtcNow;int result = now.Second + now.Hours * await task; }
6.1.3 MoveNext()
方法
MoveNext()
方法用于适时恢复、暂停状态机。它的执行逻辑如下:
- 每次
MoveNext()
方法被调用,状态机都 向前执行一步 ; - 每次执行到 await 表达式,如果 await 的值已经完成, 继续执行 ,否则状态机 暂停
MoveNext()
会在如下几种情况返回:
- 需要暂停等待一个未完成的值
- 执行流程到达了 方法末尾 或者遇到 return 语句
- 在 async 方法中有异常抛出并且 异常没有被捕获
下图是一个简化后的 MoveNext()
方法流程图(不含异常处理):
6.1.4 SetStateMachine()
方法以及状态机的装箱事宜
在状态机装箱后,该方法用于把状态机的 引用 传递给 builder,以保证后续 MoveNext()
操作在同一个状态机上执行。它的实现代码非常简单:
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{this.builder.SetStateMachine(stateMachine);
}
Eureka
装箱是为了保证后续在同一实例上操作,装箱也让我们可以获得该实例的引用。
Tips
该方法的调用在底层进行,其调用代码大致如下:
void BoxAndRemember<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IStateMachine {IStateMachine boxed = stateMachine;boxed.SetStateMachine(boxed); }
6.2 一个简单的 MoveNext()
实现
6.2.1 一个完整的具体示例
以如下代码为例,它会生成形如第二段的 MoveNext()
方法代码:
static async Task PrintAndWait(TimeSpan delay) {Console.WriteLine("Before first delay");await Task.Delay(delay);Console.WriteLine("Between delays");await Task.Delay(delay);Console.WriteLine("After second delay"); }
void IAsyncStateMachine.MoveNext()
{int num = this.state;try{TaskAwaiter awaiter1;switch (num){default:goto MethodStart;case 0:goto FirstAwaitContinuation;case 1:goto SecondAwaitContinuation;}MethodStart:Console.WriteLine("Before first delay");awaiter1 = Task.Delay(this.delay).GetAwaiter();if (awaiter1.IsCompleted){goto GetFirstAwaitResult;}this.state = num = 0;this.awaiter = awaiter1;this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this);return;FirstAwaitContinuation:awaiter1 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;GetFirstAwaitResult:awaiter1.GetResult();Console.WriteLine("Between delays");TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();if (awaiter2.IsCompleted){goto GetSecondAwaitResult;}this.state = num = 1;this.awaiter = awaiter2;this.builder.AwaitUnsafeOnCompleted(ref awaiter2, ref this);return;SecondAwaitContinuation:awaiter2 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;GetSecondAwaitResult:awaiter2.GetResult();Console.WriteLine("After second delay");}catch (Exception exception){this.state = -2;this.builder.SetException(exception);return;}this.state = -2;this.builder.SetResult();
}
6.2.2 MoveNext()
方法的通用结构
Info
以下内容会涉及如下术语:
- 快速路径:await 时已经完成,状态机将继续执行
- 慢速路径:await 时尚未完成,状态机会安排一个续延并暂停
MoveNext()
方法主要工作逻辑如下:
-
从正确的位置开始执行;
无论是在原异步代码的起始位置或者中间位置。
-
当需要暂停时,保存状态;
包括局部变量和代码中的位置。
-
当需要暂停时安排一个续延。
-
从 awaiter 获得返回值。
-
通过 builder 生成异常;
不是让
MoveNext()
自己抛出异常。 -
通过 builder 生成返回值或者完成方法。
void IAsyncStateMachine.MoveNext()
{try{switch (this.state){default: goto MethodStart;// case 的数量和 await 表达式的数量相等case 0: goto Label0A;case 1: goto Label1A;case 2: goto Label2A;}MethodStart:// 第一个 await 表达式之前的代码// 此处设置第一个 awaiterLabel0A:// 从延续中恢复执行的代码Label0B:// 快速路径和慢速路径汇合处// 剩余代码(其他标签及 awaiter 等)}catch (Exception e) //{ //this.state = -2; // 通过 builder 填充builder.SetException(e); // 所有异常信息return; //} //this.state = -2; // 通过 builder 填充builder.SetResult(); // 方法完成的信息
}
Tips
虽然
MoveNext()
中抛出的异常多数会传递给 builder,不过一些特殊的异常(如ThreadAbortException
、StackOverflowException
)会直接从MoveNext()
中抛出。
Notice
状态机中的 return 语句,与原始方法中的 return 语句含义不同:
- 状态机中的 return:状态机为 awaiter 安排延续暂停后,调用该语句
- 原方法的 return:表示方法完成,对应 try/catch 块外部的 return 语句。
6.2.3 详探 await 表达式
-
通过调用
GetAwaiter()
来获取 awaiter ,并将其保存到 栈 上。 -
检查 awaiter 是否已经完成。如果完成,则可以直接跳转到结果获取(第 9 步)。
这是 快速 路径。
-
如果是慢速路径,通过状态字段来记录 当前执行位置 。
-
使用一个字段记录 awaiter。
-
使用 awaiter 来安排一个 续延 ,保证当续延执行时,能够回到正确的状态(根据需要执行装箱操作)。
-
从
MoveNext()
方法返回到 原始调用方 (如果是第一次暂停),或者返回到 续延安排者中 。 -
当续延调起时,把状态设为正在执行(−1)。
-
把 awaiter 从字段中复制到 栈 中,清理字段(帮助回收垃圾)。
-
从 awaiter 从获取结果,该结果位于 栈 上。
这一过程与快速路径或慢速路径无关。即便没有结果值,也需要调用
GetResult()
,以便在必要时 awaiter 可以填充错误信息。 -
执行剩余原始代码。
可以使用异步操作所返回的值。
至此我们回头看上一节的代码(已截取):
MethodStart:awaiter1 = Task.Delay(this.delay).GetAwaiter();if (awaiter1.IsCompleted){goto GetFirstAwaitResult;}this.state = num = 0;this.awaiter = awaiter1;this.builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this);return;
FirstAwaitContinuation:awaiter1 = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetFirstAwaitResult:awaiter1.GetResult();
这些步骤有包含了若干细节:
-
builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this)
的调用是 装箱 操作的一部分,它有一个回调方法SetStateMachine()
某些时候调用的是
AwaitOnCompleted()
,而非AwaitUnsafeOnCompleted()
,具体细节见6.5 再探自定义 task 类型 -
num
局部变量的存在只是出于优化的目的,该变量的读取都可以视作 this.state
的读取。
6.3 控制流如何影响 MoveNext()
6.3.1 await表达式之间的控制流
本节的讲解将以如下代码为例,它增加了一个循环控制流程:
static async Task PrintAndWaitWithSimpleLoop(TimeSpan delay)
{Console.WriteLine("Before first delay");await Task.Delay(delay);for (int i = 0; i < 3; i++){Console.WriteLine("Between delays");}await Task.Delay(delay);Console.WriteLine("After second delay");
}
相较6.2.1 一个完整的具体示例中的无循环代码,编译器生成的代码对比如下:
GetFirstAwaitResult:awaiter1.GetResult();Console.WriteLine("Between delays");TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();
GetFirstAwaitResult:awaiter1.GetResult();for (int i = 0; i < 3; i++){Console.WriteLine("Between delays");}TaskAwaiter awaiter2 = Task.Delay(this.delay).GetAwaiter();
可以看到二者的差别仅是多了一个普通循环。
6.3.2 在循环中使用 await
本节的讲解将以如下代码为例,它仅有一个 await,放在了循环中:
static async Task AwaitInLoop(TimeSpan delay)
{Console.WriteLine("Before loop");for (int i = 0; i < 3; i++){Console.WriteLine("Before await in loop");await Task.Delay(delay);Console.WriteLine("After await in loop");}Console.WriteLine("After loop delay");
}
编译器利用 goto 语句将 for 循环进行拆解,新增的标签涵盖了如下 4 个功能:
- 循环初始化
- 循环条件判断
- 循环体
- 自增运算
如下是对应的代码示例:
switch (num){default:goto MethodStart;case 0:goto AwaitContinuation;}
MethodStart:Console.WriteLine("Before loop");this.i = 0; // for 循环初始化goto ForLoopCondition; // 跳转至 for 循环的条件检查
ForLoopBody: // for 的循环体Console.WriteLine("Before await in loop");TaskAwaiter awaiter = Task.Delay(this.delay).GetAwaiter();if (awaiter.IsCompleted){goto GetAwaitResult;}this.state = num = 0;this.awaiter = awaiter;this.builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);return;
AwaitContinuation: // 状态机恢复时的跳转位置awaiter = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetAwaitResult:awaiter.GetResult();Console.WriteLine("After await in loop");this.i++; // for 循环中的自增运算
ForLoopCondition: // for 循环的条件检查if (this.i < 3){goto ForLoopBody;}Console.WriteLine("After loop delay");
6.3.3 在 try/finally 块中使用 await 表达式
Info
C#5 只支持在 try 块中使用 await,C#6 解除了这一限制,支持在 catch、finally 块中使用。
本节的讲解将以如下代码为例,有一个 await 语句位于 try 块中:
static async Task AwaitInTryFinally(TimeSpan delay)
{Console.WriteLine("Before try block");await Task.Delay(delay);try{Console.WriteLine("Before await");await Task.Delay(delay);Console.WriteLine("After await");}finally{Console.WriteLine("In finally block");}Console.WriteLine("After finally block");
}
下面是编译器生成的代码,它有如下特点:
-
try 块前有一个 标签 ,try 块内包含另一个 switch 语句
在 C# 和 IL 中,都不允许从外部直接跳转到 try 块内部,因此编译器通过额外的 标签 和 switch 语句,实现从外部跳入 try 块内部的功能。作者将该技巧称为“蹦床”
-
finally 块添加了一个 if 判断
MoveNext()
的返回≠原 async 方法执行完毕。此处借助 num 值判断 是否为原 async 方法执行结束 。
switch (num){default:goto MethodStart;case 0:goto AwaitContinuationTrampoline; // 跳转至蹦床之前,以便跳转到正确位置}
MethodStart:Console.WriteLine("Before try");
AwaitContinuationTrampoline:
try
{switch (num) //{ //default: //goto TryBlockStart; // try 块中的蹦床case 0: //goto AwaitContinuation; //} //
TryBlockStart:Console.WriteLine("Before await");TaskAwaiter awaiter = Task.Delay(this.delay).GetAwaiter();if (awaiter.IsCompleted){goto GetAwaitResult;}this.state = num = 0;this.awaiter = awaiter;this.builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);return;
AwaitContinuation: // 真正的延续目标awaiter = this.awaiter;this.awaiter = default(TaskAwaiter);this.state = num = -1;
GetAwaitResult:awaiter.GetResult();Console.WriteLine("After await");
}
finally
{if (num < 0) // 该判断用于暂停期间忽略 finally 块{Console.WriteLine("In finally block");}
}
Console.WriteLine("After finally block");
6.4 执行上下文和执行流程
#suspend#看不懂,略
6.5 再探自定义 task 类型
现在,我们回头看5.8.2 剩下 0.1% 的情况:创建自定义 task 类型中的自定义 Task,每个方法的作用我们都做了讲解:
-
Create()
方法:由桩方法调用,用于创建 builder 实例。 -
AwaitOnCompleted()
、AwaitUnsafeCompleted()
方法:状态机内部会对每个 await 表达式创建一个该方法的调用(二者选其一)。方法内部会调用IAsyncStateMachine.SetStateMachine()
方法,进而调用 builder 的SetStateMachine()
方法,完成 装箱 。 -
SetException()
、 SetResult()
方法:指示异步操作完成。
public class CustomTaskBuilder<T>
{public static CustomTaskBuilder<T> Create();public void Start<TStateMachine>(ref TStateMachine stateMachine)where TStateMachine : IAsyncStateMachine;public CustomTask<T> Task { get; }public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)where TAwaiter : INotifyCompletionwhere TStateMachine : IAsyncStateMachine;public void SetStateMachine(IAsyncStateMachine stateMachine);public void SetException(Exception exception);public void SetResult(T result);
}