C# 异步方法状态机 C# async/await在底层是如何编译的

来源:这里教程网 时间:2026-02-21 17:42:25 作者:

async 方法会被编译成状态机类

C# 编译器(Roslyn)遇到

async
方法时,不会生成普通方法体,而是重写为一个实现了
IAsyncStateMachine
接口的私有嵌套结构体(或类,取决于是否捕获局部变量),这个类型就是“状态机”。它包含所有局部变量、参数、await 点的暂存字段,以及
MoveNext()
方法——所有 await 逻辑都收束到这个方法里。

常见误区是以为

await
直接调用线程切换,其实它只是在状态机中记录当前执行位置(
state
字段),然后返回一个未完成的
Task
,后续靠
GetAwaiter().OnCompleted()
注册回调来驱动状态机继续执行。

状态机字段名如
1__state
t__builder
<paramname>5__1</paramname>
都是编译器自动生成,不可见但可通过反编译(如 ILSpy)观察
如果方法不包含
await
,编译器仍会生成状态机,但会优化为同步完成(
state = -2
表示已完成)
捕获
this
或局部引用类型变量时,状态机会升格为
class
(避免装箱/生命周期问题),值类型参数则按需拷贝

Builder 模式控制 Task 构建与完成

每个

async
方法开头都会初始化一个
AsyncTaskMethodBuilder<tresult></tresult>
(或
AsyncVoidMethodBuilder
),它封装了
Task
的创建、异常传播、同步/异步完成逻辑。真正的
Task
实例直到第一次
await
或方法退出才被真正构造或设置结果。

关键点在于:状态机不直接 new Task,而是通过 builder 的

SetResult()
SetException()
SetStateMachine()
等方法间接控制任务生命周期。

SetStateMachine(this)
必须在
MoveNext()
首次调用前执行,否则
Task
无法正确绑定状态机
对于
async void
,使用
AsyncVoidMethodBuilder
,异常会直接抛给同步上下文(SynchronizationContext),无法被外层
try/catch
捕获
builder 的
Task
属性在未完成前返回的是懒构造的占位符,不是真实运行时
Task

await 表达式展开为 GetAwaiter + OnCompleted + UnsafeOnCompleted

await
不是语法糖,而是一组确定的模式调用:编译器先调用表达式的
GetAwaiter()
,再检查返回值是否实现
INotifyCompletion
(常用
TaskAwaiter
),然后插入
OnCompleted(action)
或更底层的
UnsafeOnCompleted(action)
注册回调。

这个回调函数正是状态机的

MoveNext()
,它被包装进委托并传入。当 awaitable 完成时,该委托被调度执行,状态机从上次暂停的
state
值继续运行。

如果 awaitable 已完成(
IsCompleted == true
),编译器可能跳过注册,直接同步执行后续代码(“快速路径”)
UnsafeOnCompleted
绕过栈探测和委托分配,在 hot path 上性能更高,但要求回调必须是无栈捕获的纯状态机调用
自定义 awaitable 必须提供
GetAwaiter()
+
IsCompleted
+
OnCompleted()
/
UnsafeOnCompleted()
+
GetResult()
才能被
await
消费

调试时看到的“假堆栈”和状态机实例泄漏

在调试器中单步进入

async
方法,看到的堆栈常含
MoveNext
和大量
<xxx>d__N</xxx>
类型,这就是状态机实例。它生命周期往往长于方法调用本身——比如 await 一个网络请求时,状态机实例会一直存活,直到响应返回并执行完后续逻辑。

这意味着:状态机字段若持有大对象(如

byte[]
Stream
),且未显式置空,就可能造成内存滞留;同时,调试器显示的“当前行”其实是状态机里的 goto 标签位置,不是原始源码顺序。

反编译后可见
switch(state)
块,每个
case
对应一个 await 暂停点之后的恢复位置
状态机实例默认不实现
IDisposable
,无法用
using
释放,需靠 GC 回收;若需提前清理,应在
finally
块中手动清空大字段
在高并发短生命周期场景(如 ASP.NET Core 中间件),过多状态机分配会影响 GC 压力,此时可考虑
ValueTask
+ 同步完成优化

相关推荐