async 方法编译后生成的状态机长什么样
你写的
async Task<int></int>方法,C# 编译器(Roslyn)不会直接执行它,而是重写为一个状态机类,继承自
IAsyncStateMachine。这个类包含字段:保存局部变量、参数、awaiter、当前
state,以及一个
MoveNext()方法驱动状态流转。
关键点在于:所有 await 之前的代码、每个 await 之后的“延续”逻辑,都被拆成不同 state 分支,由
switch (state)调度。局部变量(包括
this、参数、中间结果)全部被提升(lifted)为状态机字段,以保证跨 await 暂停后仍可访问。
await表达式本身被替换为
GetAwaiter()+
IsCompleted判断 +
OnCompleted()注册回调 如果 awaiter 的
IsCompleted == true(如已完成的
Task.FromResult),则跳过挂起,直接执行后续逻辑(同步完成路径) 未捕获异常会被存入状态机的
exception字段,最终在
GetResult()中重新抛出
状态机对象分配带来的堆压力
每次调用 async 方法,除非满足极严格的「热路径优化」条件(如 .NET 6+ 中的
ValueTask+ 无捕获 + 同步完成),否则都会 new 一个状态机实例 —— 这是托管堆上的对象分配,触发 GC 压力。
尤其高频调用场景(如 Web API 每请求一个
asyncaction、高吞吐消息处理循环),状态机分配会显著抬高 Gen0 GC 频率。 普通
Task-returning async 方法 → 总是分配状态机 + 可能分配
Task对象(如异步完成时) 改用
ValueTask可避免
Task分配,但状态机本身仍会分配(除非方法同步完成且无捕获) 使用
[AsyncMethodBuilder(typeof(ConfiguredValueTaskBuilder))]等自定义 builder 是高级优化手段,日常慎用
同步完成路径与 await 分支的性能差异
async 方法不是“一定慢”,它的开销集中在「需要挂起并恢复」的分支。如果 await 的操作几乎总是同步完成(例如缓存命中、内存计算、短路逻辑),那大部分调用走的是同步路径,性能接近普通方法 —— 但仍有少量额外字段访问和 switch 开销。
一旦进入异步分支(比如真正发起 HTTP 请求、磁盘读取),状态机需注册回调、上下文捕获(
SynchronizationContext/
TaskScheduler)、线程切换,延迟和内存成本明显上升。 默认情况下,
await会尝试捕获当前
SynchronizationContext(如 ASP.NET Core 早期版本),带来额外委托分配;.NET Core 3.0+ 默认禁用,大幅降低开销
await task.ConfigureAwait(false)可显式禁止上下文捕获,适用于类库或后台任务,减少委托和调度开销 过度细粒度的 await(如循环内每轮都 await 一个微小操作)会放大状态机调度成本,应合并或改用同步批量处理
如何观测真实状态机行为
别只看源码 —— 编译后的 IL 和 JIT 汇编才是真相。可用以下方式验证实际行为:
用 SharpLab 查看 C# → IL → 反编译回 C# 的状态机结构 用dotnet trace+
Microsoft-DotNETCore-EventSources采集
Microsoft-Windows-DotNETRuntime/ThreadPool/WorkerThread/Start等事件,观察线程切换频率 用
PerfView分析 GC 分配热点,确认
<yourmethod>d__X</yourmethod>类型是否高频出现在 Gen0 分配栈中
public async Task<int> GetCountAsync()
{
var data = await LoadDataAsync(); // ← 这行触发状态机拆分
return data.Length;
}上面这段代码,只要
LoadDataAsync()返回未完成的
Task,就一定会构造状态机对象,并在 await 完成后通过回调驱动继续执行
return data.Length。这个过程看似透明,但每一步都有明确的内存和调度代价。
真正影响性能的往往不是「用了 async」,而是「在不该挂起的地方挂起了」,或者「挂起后没做上下文优化」。状态机本身是机制,不是瓶颈;滥用才是问题根源。
