本文将从 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的隐式捕获,将线程调度的主动权牢牢握在自己手中。