从IL看穿async/await:状态机、同步上下文与Task
本文将从 IL(中间语言)的视角出发,剖析 async/await的底层实现。我们会一步步拆解状态机的生成、MoveNext的执行逻辑,以及 AsyncTaskMethodBuilder如何协调线程上下文与回调注册。当理解了这些底层原理后,async/await将不再是一个黑盒——你将清晰地知道它在何时挂起、如何在正确的线程上恢复、以及它究竟是如何做到“用同步的方式写异步”的。
从IL理解async/await
先看一下单独的async方法底层会有什么变化

反编译:


反编译:对于一个async函数来说,会生成了一个状态机类。
状态机类中的关键字段:
状态字段:标识当前的执行状态,初始为-1代表方法开始执行,后续为0 1 2..代表在不同的await的暂停阶段 -2代表执行完毕(成功或异常)
异步任务构建器AsyncTaskMethodBuilder <>t__builder:负责启动状态机、同步线程上下文、注册线程回调事件、返回Task…
async函数中局部变量的缓存:会把async方法中的所有局部变量都提升为状态机字段,确保之后await返回后可以让上下文恢复
对于async函数本身来说,其内部任务被固定为:
初始化:创建状态机实例,设置初始状态-1
启动:通过异步任务构建器AsyncTaskMethodBuilder的Start方法开始执行,底层调用状态机的MoveNext方法执行:MoveNext方法根据状态机对象当前的状态字段,执行对应的方法
结束:返回Task对象
加入await,并让其等待异步任务完成后:
C#源代码:


反编译:



MoveNext方法中新增逻辑:
当遇到await方法后,会根据状态机的状态执行相应的逻辑 ,在状态机处于开始状态,状态码为-1时,代表在执行主逻辑:
1.首先执行await前的逻辑,即通过Task.Run将任务交给线程池中的线程处理,并获取到Task对象,再通过GetAwaiter获取该Task对象的awaiter
2.如果任务还没执行完成,则改变当前状态机的状态,设置状态码为0,缓存该awaiter等待器用于注册后续回调事件。接着,通过异步任务构建器调用AwaitUnsafeOnCompleted(ref awaiter, ref this)方法
该方法的本质是保存执行上下文、捕获同步上下文+注册continuation(下文中的continuation指的都是await后的逻辑),并在任务完成后通过调度机制调用continuation恢复async方法的执行。 该函数会调用的两个核心方法——GetStateMachineBox和UnsafeSetContinuationForAwait
源码调用逻辑:
首先通过GetStateMachineBox将执行上下文信息和MoveNext回调方法封装保存。 具体来说,其会创建一个box对象,其作用在于:一是通过TStateMachine状态机容器保存执行上下文,即函数中的局部变量及其状态;二是其Action记录原先状态机对象的MoveNext方法,后续需要通过该box来为Task注册后续的回调逻辑
private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(ref TStateMachine stateMachine, [NotNull] ref Task<TResult> taskField) where TStateMachine : IAsyncStateMachine { ExecutionContext executionContext = ExecutionContext.Capture(); //后续复用同一个box 避免重复分配 if (taskField is AsyncStateMachineBox<TStateMachine> asyncStateMachineBox) { if (asyncStateMachineBox.Context != executionContext) { asyncStateMachineBox.Context = executionContext; } return asyncStateMachineBox; } //...... //第一次await 创建一个新的 AsyncStateMachineBox<TStateMachine>对象 AsyncStateMachineBox<TStateMachine> asyncStateMachineBox3 = ...; //存储stateMachine 保存执行上下文以及要注册的MoveNext回调 asyncStateMachineBox3.StateMachine = stateMachine; asyncStateMachineBox3.Context = executionContext; //..... return asyncStateMachineBox3; }AsyncStateMachineBox:
private class AsyncStateMachineBox<TStateMachine> : Task<TResult>, IAsyncStateMachineBox where TStateMachine : IAsyncStateMachine { //保存执行上下文的StateMahcine public TStateMachine StateMachine; //保存后续逻辑的Action对象 public Action MoveNextAction => (Action)(m_action ?? (m_action = new Action(MoveNext))); public ref ExecutionContext Context => ref Unsafe.As<object, ExecutionContext>(ref m_stateObject); //..... }核心方法UnsafeSetContinuationForAwait:
internal void UnsafeSetContinuationForAwait(IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext) { if (continueOnCapturedContex // 即:没有用 ConfigureAwait(false) { //捕获同步上下文 SynchronizationContext current = SynchronizationContext.Current; TaskContinuation taskContinuation; if (current != null && current.GetType() != typeof(SynchronizationContext)) { taskContinuation = new SynchronizationContextAwaitTaskContinuation(current, stateMachineBox.MoveNextAction, flowExecutionContext: false); } else { TaskScheduler internalCurrent = TaskScheduler.InternalCurrent; if (internalCurrent == null || internalCurrent == TaskScheduler.Default) { goto IL_0054; } taskContinuation = new TaskSchedulerAwaitTaskContinuation(internalCurrent, stateMachineBox.MoveNextAction, flowExecutionContext: false); } if (!AddTaskContinuation(taskContinuation, addBeforeOthers: false)) { //如果注册失败 则立即触发MoveNext函数 taskContinuation.Run(this, canInlineContinuationTask: false); } return; } goto IL_0054; IL_0054: if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false)) { ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal: true); } }
这是最核心的一个函数,其决定了Task后续逻辑的执行线程以及执行时机,最根本的两个判断——是否需要切回到指定的上下文以及当前Task是否执行完毕
其首先会捕获当前的同步上下文,并根据同步上下文对注册行为进行分类讨论:
对于ConfigureAwait(false)或没有设置同步上下文的对象来说:
如果注册失败,即Task已经执行完成,则立刻从线程池中选取一个线程执行stateMachineBox中的MoveNext方法;
如果注册成功,即Task还没有执行完成,则先将stateMachineBox的MoveNext方法注册到Task的回调列表中,等待Task完成后再执行
对于设置了同步上下文的对象来说:会将stateMachineBox中保存的MoveNext方法以及该同步上下文对象包装为一个Continuation对象(即SynchronizationContextAwaitTaskContinuation)
如果注册失败,则立即执行Continuation.Run方法
如果注册成功,则将Continuation.Run()注册到Task的回调列表中,等待Task执行完成后再调用。Continuation.Run()的简化逻辑如下,如果当前执行Task任务的线程就是目标线程(同步上下文正确),那么直接执行MoveNext方法;如果不在,比如Task是由线程池的线程完成的,但是MoveNext的逻辑需要会到主线程,那么调用Continuation.Post(),将MoveNext回调投放到指定线同步上下文对象的执行队列中
SynchronizationContextAwaitTaskContinuation的具体实现:runtime/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskContinuation.cs at main · dotnet/runtime
internal override void Run(Task task, bool canInlineContinuationTask)
{
if (canInlineContinuationTask &&
m_syncContext == SynchronizationContext.Current)
{
//如果当前线程 == context线程 则直接执行
RunCallback(GetInvokeActionCallback(), m_action, ref Task.t_currentTask);
}
else
{
// 如果当前线程 != context线程 则 post
m_syncContext.Post(s_postCallback, this);
}
}
以Unity为例,其会在主线程启动时就对线程上下文进行设定,那么就会调用到UnitySynchronizationContext的Post函数:
//Unity启动时对线程上下文的初始化函数
private static void InitializeSynchronizationContext()
{
var synchronizationContext = new UnitySynchronizationContext(System.Threading.Thread.CurrentThread.ManagedThreadId);
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
Awaitable.SetSynchronizationContext(synchronizationContext);
}
//https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs
关于UnitySynchronizationContext,这里给出最核心的部分:Post会将callback,即MoveNext方法注册到List中等待被调用;而Exec则会在每一帧的LateUpdate后被调用,在Unity主线程下从List中取出callback并执行
//https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs
internal sealed class UnitySynchronizationContext : SynchronizationContext
{
private readonly List<WorkRequest> m_AsyncWorkQueue;
private readonly List<WorkRequest> m_CurrentFrameWork = new List<WorkRequest>(kAwqInitialCapacity);
//...
public override void Post(SendOrPostCallback callback, object state)
{
lock (m_AsyncWorkQueue)
{
//将待执行的任务加入到等待队列中
m_AsyncWorkQueue.Add(new WorkRequest(callback, state));
}
}
public void Exec()
{
//从主队列获取所有的待执行任务
lock (m_AsyncWorkQueue)
{
m_CurrentFrameWork.AddRange(m_AsyncWorkQueue);
m_AsyncWorkQueue.Clear();
}
//依次执行所有的执行任务
while (m_CurrentFrameWork.Count > 0)
{
WorkRequest work = m_CurrentFrameWork[0];
m_CurrentFrameWork.RemoveAt(0);
work.Invoke();
}
}
}
总结一下:
当编译器遇到async关键字时,会为目标方法生成一个实现了 IAsyncStateMachine接口的状态机类。该类中包含几个关键字段:
状态字段:用一个整数标识当前执行到的位置。初始值为 -1表示方法刚开始执行;遇到第一个 await时变为 0,第二个变为 1,以此类推;值为 -2表示方法执行完毕(无论成功还是异常)。
异步任务构建器 AsyncTaskMethodBuilder <>t__builder:这是整个机制的核心调度器,负责启动状态机、管理线程上下文切换、注册回调事件,以及在任务完成时设置结果或异常。
局部变量缓存:async方法中的所有局部变量都会被提升为状态机类的字段,保存在 stateMachineBox中。这样,当方法因 await挂起并返回后,再次恢复执行时能够还原完整的上下文环境。
而await关键字的作用是指定挂起点,当所等待的异步操作尚未完成时,await会调用AwaitUnsafeOnCompleted(ref awaiter, ref this)方法,保存执行上下文+捕获同步上下文+注册continuation,随后立即返回;待异步操作完成后,再从之前保存的位置继续执行。
综上所述,async函数的实际执行流程变为:创建并初始化状态机实例 → 启动状态机 → 在合适的时机与正确的线程上下文中反复调用 MoveNext方法,驱动状态机在各个状态之间转换,直至方法执行完毕。
应用场景
先看一组概念辨析:异步、多线程、协程和async/await之间的的关系是什么?
异步是一种编程方式,其核心目标是不阻塞主线程,让任务非阻塞地执行
而多线程和协程属于实现异步的具体方式:多线程通过多个任务来并行地执行任务,但是其并不是实现异步的唯一手段,比如IO异步并不需要多线程、Unity协程也不需要多线程,其本质上是在主线程中,将一个函数分时分步地拆为多段执行(本质上不属于真正的异步执行模型)
而async/await是一种语法糖,其通过状态机对象、上下文保存以及回调注册机制统一封装所有的异步形式,让我们用同步的方式来写异步代码。
另外,关于异步IO:异步IO之所以单线程就可以完成,是因为真正的工作由操作系统/硬件完成,而线程只是负责“发起”和“被通知完成”。比如最基础的非阻塞轮询:
fcntl(fd, O_NONBLOCK); // 将文件描述符设置为非阻塞模式
read(fd, buffer); // 尝试读取数据,如果没有数据立即返回
再进阶一些的IO多路复用(select/poll/epoll),你注册一堆fd,内核中维护一个“等待队列”,线程调用epoll_wait进入休眠,在某个fd就绪时内核唤醒线程,这个过程中内核帮我们完成了监听工作,不再需要我们主动轮询,而是变为事件驱动。真正的异步IO即类似于io_uring,由于本人了解不多,所以不过多介绍
那async/await的作用究竟是什么?
async/await并不会提高计算速度,也不会缩短任务的执行时间。真正对性能的提升、对吞吐量的增大都来自于异步机制,比如异步IO、多线程,而async/await本质上只是对基于Task的异步模型的语言级别的封装,回调注册理论上可以通过ContinueWith替代,线程同步上下文的切换理论上也可以通过TaskShedular、Post方法来替代。
Task.Run(() => DoWork())
.ContinueWith(t =>
{
// continuation
}, TaskScheduler.FromCurrentSynchronizationContext());
而async/await的真正价值在于提高代码的可维护性、降低编写时的复杂度。在执行较多有依赖顺序的CPU密集型/IO密集型操作时(或需要切换同步上下文),如果我们只用Task.Run/IO异步API,那么需要大量的ContinueWith(可能还要TaskSchedular),可读性差;而通过async/await + Task.Run,则可以让顺序表达更为简单,避免异步回调地狱
Task.Run(A).ContinueWith(_ => B()).ContinueWith(_ => C());
await A();
await B();
await C();
具体展开async/await的应用场景:
对于IO密集型操作:比如调用第三方库、读取硬盘文件、读取数据库等,适合async/await+异步IO接口。这里再强调一次,性能的提升本质上来源于底层的异步IO机制,其防止主线程被阻塞,提高程序的吞吐量,async/await只是一个语法糖。
吞吐量?
比如说做一个任务需要5s,则还是需要5s,只不过如果这个任务是一个本地计算任务1s+网络异步请求4s,则可以通过异步IO让主线程在等待网络请求结果返回的这4s内,返回并继续完成其余的本地计算任务。其将原本闲置的4s利用了起来,所以才会对程序效率有提升,但是本身完成一个任务的时间是没有变的
而这个过程中,async/await机制只是负责注册continuation回调并让出线程,其并不是必须的,只是方便写这种回调流程,可读性更好,让异步代码看起来像同步
var data = await File.ReadAsync(...);
注意不是await+Task.Run(()=>{异步API接口}),其相当于是专门从线程池中取出一个线程阻塞挂起,只不过不是让主线程阻塞等待,而是让线程池中的线程阻塞等待,只起到了防止了Unity主线程被阻塞的作用(可以联系之前提到的对Task的分类)
对于计算密集型操作:比如负责解密加密、寻路算法等任务,应该选择单开一个线程来完成,充分利用多核CPU的并行优势,避免主线程阻塞
如果关心线程运行最后的结果或者需要控制多个计算任务的顺序,那么应该考虑和async/await搭配使用:可以较为方便地得到最终的结果并且优雅地安排多个任务的顺序
int result = await Task.Run(() => DoHeavyWork());
UseResult(result);
如果你不关心执行结果和顺序,只是需要在后台执行,则不如直接Task.Run
以上的讨论针对的都是不涉及到Unity相关内容操作的情况下,如果涉及到Unity相内容:同样的,对于CPU密集型则单开线程,对于IO密集型则直接await等待,但是要通过async/await保证Unity相关内容回到主线程下完成,否则需要自己保证回到主线程下,比如:
async Task Foo()
{
// 1. 主线程准备数据
var input = Prepare();
// 2. 后台线程计算
var result = await Task.Run(() => Compute(input));
// 3. 主线程应用结果
Apply(result);
}
async Task Load()
{
var data = await httpRequest;
Apply(data); // 回主线程
}
如果任务强依赖于游戏循环,比如每一帧要生成多少物体、动画过渡、UI的淡入淡出、延迟执行等操作则可以考虑使用Unity的协程
其本质上是机制的不同,协程的调度完全依赖于游戏生命周期循环,而async/await的continuation则通过UnitySynchronizationContext被投递回到主线程等待调用
重新认识Task
async/await是对基于Task的异步模型的语言级别的封装重新,为了更好地理解async/awiat,所以有必要再理解一下Task:Task并不等同于一个线程,它的作用在于承载待执行的委托、维护continuation、记录任务完成的情况以及结果。
之所以常常会将Task理解为一个线程,是因为Task.Run的影响,实际上Task.Run只是把Task中的Action交给线程来执行。具体来说,其首先创建一个Task并交给TaskSchedular来安排执行,而TaskShecular默认是从线程池中取出一个线程用来执行Task中的Action,在任务执行完成后调用Task.SetResult将结果设置进Task中,并继续执行其continuation逻辑(比如await中为其注册的MoveNext/Continuation.Run)。
实际上,Task可以分为两类,一类是主动执行,比如Task.Run创建的Task内部持有实实在在的委托对象并由TaskScheduler从线程池中选线程来执行委托;另一类是被动完成,比如async返回的Task,其本质上更像是一个“承诺”,其本身并不包含委托执行实际的逻辑,而是由状态机对象和awaiter驱动,最终通过SetResult标记完成,外界获取“承诺”的结果
尽管这两种Task对外都表现为完成标志,但是在执行过程中的角色是不同的
以async/await下执行Task.Run为例,将二者进行区分,顺便说清async函数的返回值究竟是什么:

await Task.Run()的Task,其实实在在地包含着委托并被执行,并会为其注册conitnuation逻辑;而async是异步任务构建器AsyncTaskMethodBuilder中的Task类型的字段,该字段在异步任务调度器被创建(Create()方法)时赋值,核心作用是通知异步工作完成,可以理解为一个“承诺”。 当Task.Run的Task驱动状态机工作完成时,会调用SetResult方法该Task标记为Completed状态或是异常状态。如果在原先的async函数的返回值是Task<数据类型>,那么会在状态机工作完成时,调用SetResult(结果)设置进要返回的Task中,此时外界等待结束,获取到结果
反编译解析工作流程async/await:

我们在大多数时候使用的都是“主动执行”的Task,那这些“被动完成”的Task对我们来说有什么用,又该怎么用呢?
TaskCompletionSource(TCS):
主要的作用——创建一个“被动完成”的Task,其可以由我们控制该Task何时完成、完成状态(成功、失败、取消)、完成结果等,即SetResult/SetException/SetCancel方法,其同样也不会执行任何“任务代码”。如果将“被动完成”的Task理解为一个“承诺”,那么TCS就像是承诺人,决定了在何时兑现、兑现内容是什么
为什么需要TCS?
很多异步行为都不是通过async/await写出来的,而是通过回调事件驱动实现的,比如Unity的各种资源加载、网络接收都是通过协程或者回调实现的,这些内容都是不能直接await的,TCS的用处就在于,把回调式的异步转成Task异步,从而可以await。
假设你有一个只提供“回调”的异步 API:
void DownloadAsync(string url, Action<string> onDone);
但是,你想让他支持async/await,就可以通过TCS来实现:
Task<string> DownloadAsync2(string url)
{
var tcs = new TaskCompletionSource<string>();
DownloadAsync(url, (result) =>
{
tcs.SetResult(result); // 手动完成 Task
});
return tcs.Task;
}
使用方法:
string data = await DownloadAsync2("http://example.com");
另外,原生的Task实际上也是有SetResult/SetException这些方法的,只不过没有直接暴露给用户使用,而是需要通过TCS控制。
总结
至此,我们已经从 IL 层面分析 async/await+Task的底层机制。然而,在实际的游戏开发框架中,这种“通用性”有时反而成了负担。当我们需要精确控制每个异步操作的执行线程、追求极致的 GC 性能和确定性的调度顺序时,原生 async/await的灵活性就显得有些“过度”了。
这正是下一篇文章将要探讨的主题——Fantasy 框架中的 FTask 机制。它没有沿用原生的 Task,而是另起炉灶,设计了一套轻量级的异步模型。接下来,我们将看到 Fantasy 如何通过自定义的 AsyncFTaskMethodBuilder和 FTask,绕开 SynchronizationContext的隐式捕获,将线程调度的主动权牢牢握在自己手中。