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+ 同步完成优化
