做了这么多年C#上位机和工控软件,我第一次接触async/await时也只把它当成“不会卡界面的魔法”,直到有一次排查扫码枪数据偶发性丢失的bug,才被逼着把底层状态机翻了个底朝天。那次问题最后定位到的是异步续体的线程上下文切换,不拆状态机根本看不出来。从那时起我就有个习惯:凡是面试问异步,或者团队里有人对async/await理解模糊,我都会先让他回答一个问题——“编译器把你的async方法变成了什么?”能答上来的,异步代码基本不会写出什么大坑;答不上来的,多半还在靠运气写并发。
这篇文章就围绕这个问题展开,把C# async/await底层的隐藏状态机拆开揉碎。内容不堆概念,直接看编译器生成的代码长什么样,状态机的状态字段怎么流转,Builder和Awaiter在中间扮演什么角色,以及最常见的死锁、async void、ConfigureAwait误用是怎么从状态机机制里长出来的。适合做上位机开发、工业通讯、Socket服务、机器视觉集成、以及所有需要在真实项目里处理异步并发的C#工程师。看完之后,你再看调试器里那些奇怪的异步栈帧,就不会觉得它们是乱码了。
1. 先看现象再挖底层:async/await的一秒钟直觉
1.1 async/await到底干了什么
很多开发者在写代码时,对async/await的理解停留在“方法签名加async,耗时操作前加await,界面就不卡了”。这句话在结果层面没有错,但它掩盖了一个关键事实:async/await不是运行库提供的功能,而是编译器帮你重写了代码。重写的产物,就是一个状态机。C#编译器在编译每个async方法时,会生成一个继承自IAsyncStateMachine的私有嵌套类,然后把你的方法体拆散,塞进这个类的MoveNext方法里。
为什么需要拆?因为async方法有一个非常反直觉的特性:它不是一个“从头执行到尾”的方法,而是“分段执行”的方法。每遇到一个await,方法就会暂时返回;等到await的东西完成后,方法再从上次离开的地方继续。这就要求编译器必须把所有局部变量的值、当前执行到哪一行、以及await的中间结果都保存下来。这些东西保存在哪里?就是状态机类里的字段。换句话说,编译器把普通方法里属于栈帧的东西,全部搬到了堆上的一个对象里。
这一搬,就决定了后面很多行为。比如局部变量不再一定分配在线程栈上,方法可以安全地在一个线程上启动,在另一个线程上继续执行。再比如,如果你在一个循环里频繁调用async方法,每次调用都可能产生一个新的状态机对象,这就是分配压力来源之一。理解这一点,是理解后面所有异步坑的基础。
1.2 编译器把async方法变成了什么
为了直观看到状态机的样子,先写一个最简单的async方法:
csharp复制public async Task<int> GetValueAsync()
{
Console.WriteLine("开始");
await Task.Delay(100);
return 42;
}
用ILSpy或dnSpy把编译后的DLL反编译,你会看到编译器生成了一个类似下面的嵌套类(实际命名是<GetValueAsync>d__0,这里做了简化):
csharp复制private sealed class <GetValueAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public TaskAwaiter <>u__$awaiter2;
public int <result>5__1;
private void MoveNext()
{
int num = <>1__state;
try
{
if (num != 0)
{
Console.WriteLine("开始");
TaskAwaiter awaiter = Task.Delay(100).GetAwaiter();
if (!awaiter.IsCompleted)
{
<>1__state = 0;
<>u__$awaiter2 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__$awaiter2;
<>u__$awaiter2 = default(TaskAwaiter);
<>1__state = -1;
}
awaiter.GetResult();
result = 42;
<>t__builder.SetResult(result);
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
}
}
}
别被这些奇怪的字段名吓到,逐一解释就清晰了。<>1__state是当前执行到第几个await点;<>t__builder是AsyncTaskMethodBuilder<>u__$awaiter2和后面的<result>5__1是被提升为字段的局部变量和等待器。方法体被拆成了以await为界的两段:第一段从方法开始到第一个await之前,第二段从await完成之后到方法结束。如果方法里有多个await,就会被拆成更多段,由MoveNext里的switch(num)来分发。
所
