第6章 异步原理

第6章 异步原理

6.1 生成代码的结构

异步模式的实现原理是基于 状态机 的,它负责追踪 async 方法当前的执行进度。从逻辑上讲,可以分为以下 4 种状态:

  • 未启动
  • 正在执行
  • 暂停
  • 完成(成功或 faulted)

Eureka

这里的“暂停”,指程序运行至 await 处,任务未完成时,当前方法在此处挂起(暂停)。

async 方法中的每个 await 表达式是单独的状态,每次返回后都会触发后续代码的执行。只有当状态机需要进入 暂停 时,才需要记录状态(记录状态旨在从当前执行位置恢复执行)。下图演示了不同状态之间的转换关系:

image

下面两段代码演示了原代码和编译器转化后的代码(有删改):

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()​ 方法用于适时恢复、暂停状态机。它的执行逻辑如下:

  1. 每次 MoveNext()​ 方法被调用,状态机都 向前执行一步
  2. 每次执行到 await 表达式,如果 await 的值已经完成, 继续执行 ,否则状态机 暂停

MoveNext()​ 会在如下几种情况返回:

  • 需要暂停等待一个未完成的值
  • 执行流程到达了 方法末尾 或者遇到 return 语句
  • 在 async 方法中有异常抛出并且 异常没有被捕获

下图是一个简化后的 MoveNext()​ 方法流程图(不含异常处理):

image

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 表达式

  1. 通过调用 GetAwaiter()​ 来获取 awaiter ,并将其保存到 上。

  2. 检查 awaiter 是否已经完成。如果完成,则可以直接跳转到结果获取(第 9 步)。

    这是 快速 路径。

  3. 如果是慢速路径,通过状态字段来记录 当前执行位置

  4. 使用一个字段记录 awaiter。

  5. 使用 awaiter 来安排一个 续延 ,保证当续延执行时,能够回到正确的状态(根据需要执行装箱操作)。

  6. MoveNext()​ 方法返回到 原始调用方 (如果是第一次暂停),或者返回到 续延安排者中

  7. 当续延调起时,把状态设为正在执行(−1)。

  8. 把 awaiter 从字段中复制到 中,清理字段(帮助回收垃圾)。

  9. 从 awaiter 从获取结果,该结果位于 上。

    这一过程与快速路径或慢速路径无关。即便没有结果值,也需要调用 GetResult()​,以便在必要时 awaiter 可以填充错误信息。

  10. 执行剩余原始代码。

    可以使用异步操作所返回的值。

至此我们回头看上一节的代码(已截取):

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);
}
posted @ 2025-04-01 08:57  hihaojie  阅读(43)  评论(0)    收藏  举报