第6章 异步原理
第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; // 异步基础架构类型所关联的 builder
private TaskAwaiter awaiter; // 恢复执行时用于获取结果的 awaiter
public 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; // 异步基础架构类型所关联的 builder
private TaskAwaiter awaiter; // 恢复执行时用于获取结果的 awaiter
public 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 表达式之前的代码
// 此处设置第一个 awaiter
Label0A:
// 从延续中恢复执行的代码
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 : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>
(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine;
public void SetStateMachine(IAsyncStateMachine stateMachine);
public void SetException(Exception exception);
public void SetResult(T result);
}

浙公网安备 33010602011771号