《Async in C# 5.0》第十四章 深入探讨编译器对于async的转换

C# 编译器借助 .NET 框架基类库实现了异步功能。运行时本身无需任何更改即可支持异步。这意味着 await 是被转换成了某种使用早期版本的 C# 我们自己就可以编写的东西。我们可以使用 .NET Reflector 这样的反编译器来查看生成的代码。

除了单纯的感兴趣之外,理解这些生成的代码还有助于调试、性能分析以及对异步代码进行其他诊断。

存根方法(Stub Method)

异步方法被替换为存根方法。调用异步方法时,首先运行的是存根方法。让我们以这个简单的异步方法为例:

cs 复制代码
public async Task<int> AlexsMethod() 
{
    int foo = 3;
    await Task.Delay(500);
    return foo;
}

编译器生成的存根方法如下所示:

cs 复制代码
public Task<int> AlexsMethod()
{
    <AlexsMethod>d__0 stateMachine = new <AlexsMethod>d__0();
    stateMachine.<>4__this = this;
    stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start<<AlexsMethod>d__0>(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

为了使其更容易理解,我对变量名称进行了一些修改。

正如我们在第四章 "Async、方法签名和接口"中看到的,async关键字对于该方法如何被外部使用没有影响。这一点很明显------你可以看到存根方法的签名始终与原始异步方法相同,只是没有了 async 关键字。

你会注意到,存根方法中并没有我写的代码(方法AlexMethod的具体实现)。存根方法的大部分工作是初始化了一个名为 <AlexsMethod>d_0 的结构体的变量。该结构体是一个状态机,所有繁重的工作都在这里完成。存根方法调用 Start 方法,然后返回一个 Task。为了理解里面发生了什么,我们需要查看状态机结构体本身的实现细节。

状态机结构体

编译器会生成一个结构体来充当状态机,并包含我原始方法的所有代码。这样做是为了创建一个能够表示方法状态的对象,以便在执行到 await 时存储该状态。请记住,当我们到达 await 时,方法中所有关于当前位置的信息都会被记录下来,以便在方法恢复时使用。

在方法暂停时(译者注:到达 await 时),虽然编译器可以遍历并存储方法中的每个局部变量,但这会生成大量代码。更好的做法是将方法的所有局部变量更改为某个类型的成员变量,这样我们只需存储该类型的实例,它的所有局部变量也会自动保留,这正是这个结构体出现的原因。

小贴士

出于性能考虑,状态机被定义为一个结构体而不是一个类。这意味着当异步方法以同步的方式完成时,它不需要被分配到堆上。不幸的是,作为一个结构体,它使我们更难进行推理。

状态机是一个包含了异步方法的类型的内部结构体。所以很容易确定它是从哪个方法生成的,但这样做的主要目的是为了能够访问该类型的私有成员。

让我们看一下根据示例生成的状态机结构体 <AlexsMethod>d_0。现在我们来看看它的成员变量:

cs 复制代码
public int <>1__state;
public int <foo>5__1;
public AlexsClass <>4__this;
public AsyncTaskMethodBuilder<int> <>t__builder;
private object <>t__stack;
private TaskAwaiter <>u__$awaiter2;

小贴士

所有变量名中都有尖括号。这仅仅是为了标记它们是编译器生成的。当编译器生成的代码必须与用户代码共存时,用尖括号区分很重要,因为在有效的 C# 中,变量不能使用尖括号。但在这里,这并不是必要的。

首先,状态变量 <>1__state 用于存储我们到达的 await 编号。在到达任何 await 语句之前,它的值为 -1。原始方法中的每个 await 语句都有编号,当方法暂停时,要恢复的 await 语句编号会被赋给这个状态变量。(译者:这个名字起的不好,应该叫 __indexOfAwaitToResume 才好)

接下来是 <foo>5__1,它存储了原始变量 foo 的值。我们很快就会看到,所有对 foo 的访问都会被替换为对该成员变量的访问。

然后是 <>4__this。它仅出现在非静态异步方法的状态机中,并包含异步方法所属的对象。在某种程度上,可以将其视为方法中的另一个局部变量,当您访问同一对象的其他成员时,它恰好会被隐式使用。异步转换之后,它需要被显式存储和使用,因为我的代码已从其它的原始对象移动到了状态机结构。

AsyncTaskMethodBuilder 是一个辅助类型,它包含了所有这种状态机共享的逻辑。它负责创建 Task 并由存根方法返回。实际上,它的作用与 TaskCompletionSource 非常相似,因为它会创建一个虚拟 Task (puppet Task),稍后Task才会完成。与 TaskCompletionSource 的区别在于,它针对异步方法进行了优化,并使用了一些技巧,例如使用结构体而不是类来提升性能。

小贴士

返回 void 的异步方法使用 AsyncVoidMethodBuilder 作为其辅助类型,而返回 Task<T> 的异步方法使用泛型版本的 AsyncTaskMethodBuilder<T>。

栈变量 <>t__stack 用于作为较大表达式一部分的 await。.NET 中间语言 (IL) 是一种基于栈的语言,因此复杂的表达式由一些小指令构成,这些小指令会操作一堆数值。当 await 位于这种复杂表达式的中间时,栈里面当前的值将存入这个stack变量中,如果存在多个值,则将其放入 Tuple 中。

最后,TaskAwaiter 变量被用来临时存储对象,该对象帮助 await 关键字注册任务完成时的通知。

MoveNext 方法

状态机有一个名为 MoveNext 的方法,所有的原始代码最终都会执行到该方法中。MoveNext 在方法首次运行时以及从 await 恢复时都会被调用。即使对于最简单的异步方法,它的实现看起来也极其复杂,因此我将尝试分步骤解释这一转换过程。我会跳过一些不太相关的细节,因此从描述上会有很多地方不完全准确。

小贴士

该方法最初之所以被称为 MoveNext,是因为它与早期 C# 版本中的迭代器块生成的 MoveNext 方法类似。这些方法使用 yield return 关键字在单个方法中实现 IEnumerable。那里使用的状态机系统与异步状态机类似,但更简单一些。

你的代码

第一步是将你的代码复制到 MoveNext 方法中。请记住,对任何变量的访问都需要指向状态机里的新成员变量。在 await 所在的位置,我先留出一个空白,稍后再进行填充。

cs 复制代码
<foo>5__1 = 3;
Task t = Task.Delay(500);
Logic to await t goes here
return <foo>5__1;

将"返回"转换为"完成"

原始代码中的每个 return 语句都需要进行转换,转换后的代码要能够完成存根方法返回的 Task。实际上,MoveNext 返回 void,所以语句 return foo; 甚至是无效的。

cs 复制代码
<>t_builder.SetResult(<foo>5__1);
return;

当然,在 Task 完成后,我们使用了 return; 退出 MoveNext 方法。

跳转到方法中的正确位置

因为 MoveNext 会在每次 await 后恢复执行时被调用,也会在方法首次启动时被调用,所以我们需要首先跳转到方法中的正确位置才能开始。这使用了类似于 switch 语句生成的 IL 代码达成,就像我们在切换状态一样。

cs 复制代码
switch (<>1__state)
{
    case -1: // Right at the start of the method
        <foo>5__1 = 3;
        Task t = Task.Delay(500);
        Logic to await t goes here
    
    case 0: // There's only one await, so it is number 0
        <>t__builder.SetResult(<foo>5__1);
        return;
}

--- <未完待续> ---

相关推荐
Tong Z1 小时前
Spring Boot 请求处理链路
java·spring boot·后端
LSL666_1 小时前
3 Redis 的 Java 客户端
java·数据库·redis
虫师c1 小时前
Spring Boot自动配置黑魔法:手写Starter实现原理深度解析
java·spring boot·后端·自动配置·starter
神明不懂浪漫2 小时前
【第十三章】操作符详解,预处理指令详解
c语言·开发语言·经验分享·笔记
MediaTea2 小时前
Python:类型槽位
开发语言·python
范什么特西2 小时前
狂神---死锁
java·前端·javascript
郝学胜-神的一滴2 小时前
深入解析Effective Modern C++条款35:基于任务与基于线程编程的哲学与实践
开发语言·数据结构·c++·程序人生
小飞学编程...2 小时前
【Java相关八股文(二)】
android·java·开发语言
程序猿阿越2 小时前
Kafka4(一)KRaft下的Controller
java·后端·源码阅读