《C# IN DEPTH》第四版 --- 关于async/await的实现章节随记

Part 2 C# 2–5

《C# IN DEPTH》
-- FOURTH EDITION

Author: Jon Skeet

如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的

6 Async implementation

6.1 Structure of the generated code

实现是以状态机的形式出现的

编译器会生成一个私有的内嵌的结构,用以代表异步方法,同时它也必须包含一个方法,这个方法与我们自己声明的方法签名一致的。这个方法叫做 sub method,它没有什么特别的,但是它启动了所有其他方法的运行

我经常会谈到状态机暂停。这对应于 async 方法到达一个 await 表达式而等待的操作尚未完成的时间点。你可能还记得在第5章中,当这种情况发生时,一个延续被安排在等待的操作完成后执行 async 方法的其余部分,然后 async 方法返回

状态机跟踪您在 async 方法中的位置。从逻辑上讲,按照通常的执行顺序,有四种状态:

  • Not started
  • Executing
  • Paused
  • Complete(either successfully or faulted)

只有暂停状态集是取决于 async 方法的结构。方法中的每个 await 表达式都是为了触发更多执行而返回的独特状态

当状态机执行时,它不需要跟踪正在执行的代码的确切位置; 在这一点上,它只是普通代码,CPU 跟踪指令指针就像同步代码一样

当状态机需要暂停时记录状态; 整个目的是允许它稍后从到达的点继续执行代码

Excample

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");
}

将上面代码的IL代码反编译成c#代码

// Stub method
[AsyncStateMachine(typeof(PrintAndWaitStateMachine))]
[DebuggerStepThrough]
private static unsafe Task PrintAndWait(TimeSpan delay)
{
    // 初始化状态机,包括方法的参数
    var machine = new PrintAndWaitStateMachine
    {
        delay = delay,
        builder = AsyncTaskMethodBuilder.Create(), 
        state = -1
    };
    // 运行状态机,直到状态机遇到需要wait的地方才会返回
    machine.builder.Start(ref machine);
    // 返回代表这个async动作的Task
    return machine.builder.Task;
}
// Private struct for the state machine
[CompilerGenerated]
private struct PrintAndWaitStateMachine : IAsyncStateMachine
{
    // 状态机的状态,也就是需要唤醒的位置,或者说需要接续执行代码的位置
    public int state;
    // builder 与 async 基础设施类型相结合
    public AsyncTaskMethodBuilder builder;
    // Awaiter,以便在恢复时取得结果
    private TaskAwaiter awaiter;
    // 原始方法的方法参数
    public TimeSpan delay;

    // 状态机主要的代码运行区域,下文单独列出
    void IAsyncStateMachine.MoveNext(){}

    [DebuggerHidden]
    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        // 连接builder和状态机
        this.builder.SetStateMachine(stateMachine);
    }
}

6.1.1 The stub method: Preparation and taking the first step

创建状态机之后,stub方法要求状态机的构建器启动它,通过引用传递状态机本身。因为效率的问题,所以是通过ref传递。通过 Start 方法传递引用状态机可以避免复制状态,这样更有效,并确保在 Start 方法返回时,对 Start 中的状态所做的任何更改仍然可见。特别是,在启动期间,状态机内的构建器状态可能会发生很大的变化

启动状态机不会产生任何新的线程。它只是运行状态机的 MoveNext()方法,直到状态机需要等待另一个异步操作或完成时导致状态机暂停的时候才会返回。换句话说,它执行了一个步骤。不管是哪种方式,MoveNext()返回,在这个时候 machine.builder.Start() 返回,将表示整个异步方法的任务返回给调用者。构建器负责创建任务,并确保任务在异步方法的过程中适当地更改状态

6.1.2 Structure of the state machine

状态机结构体中需要关注的地方:

  • 实现了 IAsyncStateMachine 接口, 该接口用于 async 基础设施,该接口只包含上面显示的两个方法
  • 那些字段保存了状态机在一个步骤和下一个步骤之间需要记住的信息
  • MoveNext()方法会在状态机启动时调用一次,然后是在每次暂停后的恢复中会调用一次
  • SetStateMachine()永远都是相同的实现

您已经看到了实现 IAsyncStateMachine 的类型的一种用法,尽管它有些隐藏: AsyncTaskMethodBuilder.Start()是一个通用方法,类型参数必须受到实现 IAsyncStateMachine 的约束。在执行一些内务管理之后,Start()调用 MoveNext()使状态机执行 async 方法的第一步

所涉及的字段可大致分为五类:

  • 当前状态(例如,不启动、在特定的 await 表达式上暂停等等)
  • 方法构建器用于与 async 基础设施通信,并提供返回的 Task
  • Awaiters
  • 参数和本地变量
  • 临时堆栈变量

状态字段比较简单,只是数字:

  • -1:未启动或当前正在执行
  • -2:完成(成功或出错)
  • 其他:在一个特定的 await 表达上暂停

如前所述,构建器的类型取决于 async 方法的返回类型

  • AsyncVoidMethodBuilder
  • AsyncTaskMethodBuilder
  • AsyncTaskMethodBuilder< T >
  • AsyncTaskMethodBuilderAttribute(自定义的task类型)

其他字段稍微复杂一些,因为它们都依赖于 async 方法的主体,而编译器试图尽可能少地使用字段。要记住的关键一点是,只有在状态机在某个时刻恢复之后需要返回的值才需要字段。有时编译器可以为多种目的使用字段,有时可以完全省略它们

编译器如何重用字段的第一个示例是awaiters。一次只有一个 awaiter 是相关的,因为任何特定的状态机一次只能 await 一个值

编译器为每个使用的 awaiter 类型创建一个字段。如果在一个 await 方法中输入两个 Task < int > 值、一个 Task < string > 和三个非 async 的 Task 值,那么最终会得到三个字段: 一个 TaskAwaiter < int > 、一个 TaskAwaiter < string > 和一个非属性 TaskAwaiter。编译器根据 await 类型为每个 awaiter 表达式使用适当的字段

接下来,让我们考虑局部变量。在这里,编译器不重用字段,但可以完全省略它们。如果局部变量只在两个 await 表达式之间使用,而不是跨 await 表达式使用,那么它可以在 MoveNext ()方法中作为局部变量保留

public async Task LocalVariableDemoAsync()
x is assigned
{
    int x = DateTime.UtcNow.Second;
    int y = DateTime.UtcNow.Second;
    Console.WriteLine(y);

    await Task.Delay();

    Console.WriteLine(x);
}

编译器将为 x 生成一个字段,因为在状态机暂停时必须保留该值,但是在代码执行时,y 可以只是堆栈上的一个局部变量

最后,还有临时堆栈变量。当一个 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 The MoveNext() method (high level)

上节解释了状态机中的所有字段。接下来,首先需要查看 MoveNext()方法ーー但仅限于概念上,因为反编译后的代码非常冗长

每次调用 MoveNext ()时,状态机都会执行一个步骤。每次到达一个 await 表达式时,如果等待的值已经完成,则继续执行,否则暂停。如果发生下列任何一种情况,MoveNext ()将返回:

  • 状态机需要暂停, await 一个未完成的值。
  • 执行到达方法或返回语句的结尾。
  • 抛出异常,但不会在 async 方法中捕获

注意,在最后一种情况下,MoveNext ()方法最终不会引发异常。相反,与 async 调用相关联的Task会出现故障

下图显示了一个关注于 MoveNext ()方法的 async 方法的一般流程图,图中没有关于错误的处理,这需要等到看到完整的代码的时候才会说明,还有就是 SetStateMachine 方法的调用也没有写出来,因为这个图已经很复杂了

关于 MoveNext ()方法的最后一点: 它的返回类型是 void,而不是Task类型。只有存根方法需要返回Task,在构建器的 Start ()方法调用 MoveNext ()执行第一步之后,存根方法从状态机的构建器获取Task。对 MoveNext ()的所有其他调用都是从暂停状态恢复状态机的基础结构的一部分,这些调用不需要相关Task

6.1.4 The SetStateMachine method and the state machine boxing dance

SetStateMachine 这个方法已经在前面展示过了,它的实现在release打包下,都是一样的。

该方法的目的很容易在较高的层次上进行解释,但是细节却很复杂。当状态机执行第一步时,它作为stub方法的局部变量在堆栈上。如果它暂停,它必须将自己装箱(放到堆上) ,以便在恢复时所有信息仍然在原处。装箱后,将使用装箱值作为参数对装箱值调用 SetStateMachine。换句话说,在基础设施的深处,有一些代码看起来有点像这样:

void BoxAndRemember<TStateMachine>(ref TStateMachine stateMachine)
            where TStateMachine : IStateMachine
{
    IStateMachine boxed = stateMachine;
    boxed.SetStateMachine(boxed);
}

事情没有那么简单,但是它传达了正在发生的事情的本质。然后,SetStateMachine 的实现确保 AsyncTaskMethodBuilder 具有对其所属的状态机的单个装箱版本的引用。该方法必须对装箱后的值进行调用; 它只能在装箱后进行调用,因为在装箱后才有对装箱后的值的引用,如果在装箱后对未装箱的值进行调用,则不会影响装箱后的值。(请记住,AsyncTaskMethodBuilder 本身是一个值类型。)这个复杂的过程确保当一个continuation委托传递给 awaiter 时,该延续将在同一个装箱实例上调用 MoveNext ()。

结果是,如果不需要,状态机根本不会被装箱,如果需要,它只会被装箱一次。装箱之后,一切都在盒装版本中进行。这些复杂的代码都是为了效率。

6.2 A simple MoveNext() implementation

这个简单实现,是不包含循环、try语句或者using语句的,只有简单的控制流,这导致了一个相对简单的状态机。

6.2.1 A full concrete example

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() method general structure

为了简洁起见,在每个 await 表达式中,等待的值可能已经完成,也可能仍然不完整。如果 await 已经完成,状态机将继续执行。我称之为 fast path 。如果还没有完成,状态机将安排延续并暂停,称之为 slow path

该方法负责以下事项:

  • 从正确的地方执行(不管是原始 async 代码的开始还是中途)
  • 在需要暂停时保留状态,包括局部变量和代码中的位置
  • 在需要暂停时安排延续
  • 从awaiters检索返回值
  • 通过构建器传播异常(而不是让 MoveNext ()本身因异常而失败)
  • 通过构建器传播任何返回值或方法完成
void IAsyncStateMachine.MoveNext()
{
    try
    {
        // As many cases as there are await expressions
        // 这个switch里面的case都是一些关于 await 表达式的
        switch (this.state)
        {
            default: goto MethodStart;
            case 0: goto Label0A;
            case 1: goto Label1A;
            case 2: goto Label2A;
        }

    MethodStart:    
        // 关于 await 表达式之前的代码
        // 设置 第一个 awaiter
    Label0A:    
        // 从 continuation 中恢复的代码,也就是 await 之后的代码
    Label0B:
        // fast path 和 slow path 的重新连接
        // 代码的剩余部分,包含更多的标签、awaiters等等
    }
    // 通过 builder 传播所有的异常
    catch (Exception e)
    {
        this.state = -2;
        builder.SetException(e);
        return;
    }
    // 通过 builder 传播方法的完成信号
    this.state = -2;
    builder.SetResult();
}

在 try/catch 块中,MoveNext()方法的开始始终是一个有效的 switch 语句,用于根据状态跳转到方法中的正确代码段。如果这个状态是非负的,那就意味着你是在一个 await 表达式之后恢复的。否则,假设您是第一次执行 MoveNext()

需要注意的一点是状态机中的 return 语句和原始 async 代码中的 return 语句之间的区别。在状态机中,当状态机在为一个 awaiter 调度一个延续之后暂停时,就会使用 return。原始代码中的任何 return 语句最终都会下降到 try/catch 块之外的状态机底部,在那里方法完成通过构建器进行传播结果

6.2.3 Zooming into an await expression

让我们再次思考一下,假设已经计算了操作数,得到了可以等待的结果,那么在执行一个 await async 方法时,每次遇到一个变量表达式时都会发生什么情况

  1. 您可以通过调用 GetAwaiter()从 awaiter 获取数据,并将其存储在堆栈中
  2. 检查 awaiter 是否已经完成。如果有,您可以直接跳到获取结果(步骤9)。这是 fast path
  3. 记住通过状态字段到达的位置
  4. 将 awaiter 记录到字段中
  5. 在 awaiter 中安排一个延续,确保当延续被执行时,你会回到正确的状态
  6. 如果这是您第一次暂停,则从 MoveNext ()方法返回到原始调用者,否则返回到任何安排的延续
  7. 当 continuation 触发时,将状态设置回运行状态(值为 -1)
  8. 将 awaiter 复制到字段外并返回到堆栈上,清除字段以便潜在地帮助垃圾收集器。现在你可以重新加入fast path
  9. 从 awaiter 中获取结果,不管你选择哪条路径,这个结果都在堆栈中。即使没有结果值,也必须调用 GetResult ()才能让 awaiter 在必要时传播错误
  10. 继续您的愉快之路,如果有结果值,则使用结果值执行原始代码的其余部分

根据上面列出的名单,下面看看那个具体的例子的第一次 await 代码发生了什么

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();

这两个标签表示您必须跳转到的两个位置,具体取决于路径

  • fast path 中,跳过 slow-path 代码
  • slow path 中,当调用延续时,您将跳回到代码的中间。(请记住,这就是方法开始处的 switch 语句的用途。)

builder.AwaitUnsafeOnCompleted(ref awaiter1, ref this) 的调用是通过调用 SetStateMachine 进行装箱(如果需要)并调度 continuation 的部分。在某些情况下,您将看到对 AwaitOnCompleted 的调用,而不是 AwaitUnsafeOnCompleted。这些差异仅在处理执行上下文的方式方面有所不同

6.3 How control flow affects MoveNext()

6.3.1 Control flow between await expressions is simple

接下来看看当在异步代码中添加了loop之后,会变成什么样子

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");
}

上述代码反编译之后的代码中,与之前的反编译相比较没有太大变化,唯一不一样的是:

// 之前
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();

状态机中的更改与原始代码中的更改完全相同。在如何继续执行方面没有额外的字段和复杂性; 它只是一个循环

我提出这个问题的原因是为了帮助您思考为什么在我们的下一个示例中需要额外的复杂性。在上面代码中,我们永远不需要从外部跳入循环,也永远不需要暂停执行并跳出循环,从而暂停状态机。这些情况是由 await 表达式引入的,当你在循环中 await 时

6.3.2 Awaiting within a loop

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");
}

stub 方法和状态机与前面的示例几乎完全一样,但是状态机中有一个额外的字段对应于循环计数器 i

您可以在 C # 中如实地表示代码,但不能使用循环结构。问题是,在状态机从暂停 Task.Delay 返回之后,您希望跳到原始循环的中间

可以使用大量 goto 语句实现 for 循环,而不需要引入任何额外的作用域。这样的话,你可以毫无困难地跳到中间去。下面的清单显示了 MoveNext()方法主体的大部分反编译代码。我只包含 try 块中的部分,因为这是我们在这里关注的

    switch (num)
    {
        default:
            goto MethodStart;
        case 0:
            goto AwaitContinuation;
    }
MethodStart:
    Console.WriteLine("Before loop");
    this.i = 0;         // 循环开始的初始化
    goto ForLoopCondition;      // 直接跳到检查循环条件
ForLoopBody:    // 循环体
    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:  // 状态机恢复之后会跳到这里,也就是 *slow path*
    awaiter.GetResult();
    Console.WriteLine("After await in loop");
    this.i++;
ForLoopCondition:   // 检查循环状态,如果持续就跳回循环体
    if (this.i < 3)
    {
        goto ForLoopBody;
    }
    Console.WriteLine("After loop delay");

6.3.3 Awaiting within a try/finally block

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");
}

即使在 IL 中,也不允许从 try 块的外部跳到它的内部。这有点像您在前面的循环小节中看到的问题,但是这次不是 C # 规则,而是 IL 规则

为了实现这一点,C # 编译器使用了一种我喜欢把它看作蹦床(trampoline)的技术。(这不是官方术语,尽管这个术语在其他地方也有类似用途。)它跳转到 try 块之前,然后 try 块中的第一件事是跳转到块中正确位置的一段代码

finally 块也需要小心处理。在三种情况下,您将执行生成的代码的 finally 块:

  • 您到达 try 块的末尾
  • Try 块引发异常
  • 由于一个 await 表达式,您需要在 try 块中暂停

如果因为暂停状态机并返回给调用者而执行 finally 块,那么原始的 async 方法 finally 块中的代码就不应该执行。毕竟,您在 try 块中逻辑上暂停了,当延迟完成时将在那里恢复。幸运的是,这很容易检测到: 如果状态机仍在执行或完成,则 num 局部变量(它总是与 state 字段相同)为负值; 如果暂停,则为非负值

switch (num)
{
    default:
        goto MethodStart;
    case 0:
        goto AwaitContinuationTrampoline;   // 跳转到刚好在蹦床之前,所以它可以反弹执行到正确的地方
}

MethodStart:
    Console.WriteLine("Before try");
AwaitContinuationTrampoline:
    try
    {
    // 在 try 区域内的蹦床
    switch (num)
    {
        default:
            goto TryBlockStart;
        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:      // 真正的 continuation
    awaiter = this.awaiter;
    this.awaiter = default(TaskAwaiter);
    this.state = num = -1;
GetAwaitResult:
    awaiter.GetResult();
    Console.WriteLine("After await");
}
finally
{
    // 如果暂停,则有效地忽略 finally 块
    if (num < 0)
    {
        Console.WriteLine("In finally block");
    }
}
Console.WriteLine("After finally block");

特别是要记住编译器可以执行许多转换来使代码比我所展示的更简单。正如我前面所说,我总是使用 switch 语句来表示“跳转到 X”代码片段,编译器有时可以使用更简单的分支代码。在读取源代码时,多种情况下的一致性非常重要,但这对编译器来说并不重要

6.4 Execution contexts and flow

这段写的莫名其妙,对于Context的具体还不理解

到目前为止,我略过的一个方面是,为什么 awaiters 必须实现 INotifyCompletion,但也可以实现 ICriticalNotifyCompletion,以及它对生成的代码的影响

在前文中,我描述了同步上下文,它们用于控制代码在其上执行的线程。上下文提供了一种透明地维护信息的环境方式

您不需要显式地传递所有这些信息; 它只是遵循您的代码,在几乎所有情况下都做正确的事情。一个类用于管理所有其他上下文: ExectionContext

ExectionContext 不是这样的,当你的 async 方法继续时,你几乎总是需要相同的执行上下文,即使它在不同的线程上

这种对执行上下文的保留称为流。一个执行上下文被认为是跨 await 表达式流动的,这意味着你所有的代码都在同一个执行上下文中运行。谁来保证这个?AsyncTaskMethodBuilder 总是这样做,TaskWaiter 有时也这样做。这就是事情变得棘手的地方

INotifyCompletion.OnCompleted 方法只是一个普通的方法; 任何人都可以调用它。相比之下,ICriticalNotifyCompletion.UnsafeOnCompleted 标记为[ SecurityKey ]。它只能由受信任的代码调用,例如框架的 AsyncTaskMethodBuilder 类

在编译 async 方法时,编译器会在每个 await 表达式处创建一个对 builder.AwaitOnCompleted 或 builder.AwaitUnsafeOnCompleted 的调用,具体取决于 awaiter 是否实现了 ICriticalNotifyCompletion

阅后感

这本书的这个章节,对于异步代码的实现,作出了一个相对清晰的描述,但是还是有很多不清楚的地方或者是作者故意掩盖的复杂性

我们现在是知道了,当我们在调用 await 的时候,是会切分成了 await 前和 await 后两段代码,当我们 await 的这个异步方法,返回的 awaiter 已经准备好了,那么就可以直接继续执行 await 后的代码,也就是它的延续或者callback,如果结果没有准备好,那么就会记录状态机,然后返回,也就是暂停状态机

按照上面的描述,其实我直觉上感觉,好像缺少了几块拼图,我感觉这个流程的流动并没有非常顺畅:

  1. 前文中知道,当一个方法中出现多个 await 平铺的时候,状态机也会有多个非负数的状态,但是一个问题是,在我们调用异步方法的时候,我们是直接调用 GetAwaiter(),然后检查它的状态,那么这个异步方法是谁在执行?是否和第一层异步方法一样,也是我们的线程进入这个第二层的异步方法中执行?(GetAwaiter这个方法还得复习一下)
  2. 根据第一点,我们继续思考,假设我们调用的异步方法是最外层的代码,那么有可能内部的代码又调用了另一个异步代码,那么假如没有理解错误,一个异步方法会是一个状态机,那么涉及异步方法的嵌套的时候,整个调用栈是不是就是有层次的状态机?还是说会将调用栈平铺,变成一个巨大的状态机?
  3. 那么继续往下,我们假设整个方法的调用是有层次的,那么在宇宙的尽头(狗头),洋葱的core里面,是什么?我猜测是不是涉及了关于 System call 的内容?
  4. 延续上一点,在关于异步的描述中,都没有明确的说明它的实现是否涉及线程池的帮助,假如没有(忘记是官网还是哪本书说的,c#的标准库的异步I/O实现,都是调用之后会马上返回,是非阻塞的),那么它是如何做到调用之后,马上返回,难道不需要一个线程在值守吗?还是说像是涉及硬件参与的真正的完全的异步(DMA)?
  5. 继续往下,假设涉及了硬件,但是在数据到达的情况下,即使有回调,那也需要有线程来执行后续内容,那么谁来做这个角色?在不能打断当时的caller线程的执行之后,我感觉还是会有线程池来完成这项工作,我猜测在硬件完成数据的搬运之后,中断发出,OS回调,然后应该有一个类似Schdule的角色,分配这个回调最终是谁(worker)来执行,也就是我们的状态机会被恢复,然后线程池中的worker接手状态机的执行
  6. 与上面的情况不一样的是,假如没有涉及硬件的完全的异步系统调用呢?比如说我知道的linux下的epoll模型(希望没说错),它是一种同步非阻塞的网络I/O模型,也就是说,必须有线程值守,但即使是这种情况,也可以用上一点的猜测,也就是有一个Schedule的角色,来完成这些工作

Schedule这个角色,是在学习Rust的异步概念和异步库Tokio的时候,它们所提出的一种运行时,而且rust的future是惰性的,类似于当我们只是调用异步方法后获得Task,而并没有使用 await 去对Task获取结果,然后在rust中这个future是还没有开始执行的,而c#的是最起码执行了 await 前的代码了,然后rust的future是需要一个运行时去poll这个future,才能让future去前进,所以我合理地猜测,c#的Task也需要一个运行时来推动状态机的前进。

可以确定的是,一些实验性的代码可以认识到具体的一个执行:

Task start() 
{
    A
    await T1
    B
}

Task T1()
{
    C
    await T2
    D
}

Task T2()
{
    E
    await T3
    F
}

Task T3()
{
    await Task.Delay()
}

假设start方法的线程名称是 current-1,然后有一个线程池的线程是 pool-1,那么整个执行的流,就会变成:

current-1: A -> C -> E
pool-1: F -> D -> B

会发现这个调用链被切分成两部分了,而且重要的是,每次只会有一条线程在整个上下文当中,所以其实平铺之后,整个运行是一种同步的形式的,只不过被切开了而已。当然,如果涉及多个await的时候,就没那么简单了,但是这让我在编写代码的时候,真就完全是用同步的方式在编写异步的代码

上面是对于c#异步代码的一些探索过程和总结,如果有什么错漏的,都可以讨论一下,最好是大佬过来剖析一下是最好不过了。书籍的内容是纯英文的,部分是机翻,部分是我自己翻译的(机翻会翻车),如果需要电子书的小伙伴也可以留下邮箱,看到会发送的,当然还有其他的电子书,有需要可以说一下,我看情况回应一下,谢谢阅读。

posted @   huang1993  阅读(212)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示