Async与Await异步模式源码剖析

作者:Chdon 发布时间: 2025-09-30 阅读量:76 评论数:0

从反编译视角看 C# async/await 的工作原理

asyncawait 是 C# 中使用最频繁的语法糖之一,简洁的几行代码背后,编译器其实为我们做了大量工作:生成状态机、托管挂起与恢复、捕获同步上下文……这些机制平时被很好地隐藏起来,但一旦遇到死锁、上下文切换或异常处理的疑难问题,了解底层实现就变得非常有价值。

本文将通过一段极简的示例代码,借助 ILSpy 反编译产物,从头到尾梳理一次 await 表达式的完整执行流程,并顺带讨论 ConfigureAwait(false) 与同步等待 Task 的相关细节。

一、示例代码与反编译产物

1.1 示例代码

我们从一段非常简单的代码开始:

using System;
using System.Threading.Tasks;

private static async Task Main(string[] args)
{
    Console.WriteLine(await Task.Factory.StartNew(() => Task.FromResult(0)));
}

代码语义很简单:启动一个新任务,等它完成后把结果打印出来。但编译器在背后生成的代码却远比这复杂。

1.2 反编译结果

使用 ILSpy 反编译时,把语言版本切换到 C# 4.0 / VS 2010,可以看到编译器生成的状态机原貌(更高版本的 C# 语法会把这些细节再次"语法糖化"隐藏起来)。

1.2.1 改写后的 Main 方法

原来的 async Main 被替换成了一个普通方法,内部负责创建并启动状态机:

[AsyncStateMachine(typeof(<Main>d__0))]
[DebuggerStepThrough]
private static Task Main(string[] args)
{
    <Main>d__0 stateMachine = new <Main>d__0();
    stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
    stateMachine.args = args;
    stateMachine.<>1__state = -1;
    stateMachine.<>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

这里有三个关键角色:

  • 状态机类 <Main>d__0:由编译器生成,保存方法的所有局部变量、参数以及"当前执行到哪一步"的状态值。
  • AsyncTaskMethodBuilder:异步方法的控制器,负责驱动状态机、产出最终返回的 Task、处理结果与异常。
  • 状态字段 <>1__state:核心调度标志,-1 表示尚未开始或刚开始,0、1、2... 对应各个 await 挂起点,-2 表示已结束。

1.2.2 状态机的核心方法 MoveNext

状态机的所有逻辑都集中在 MoveNext 方法里。原方法体中的每一个 await 表达式,都会被拆分成"挂起前"和"恢复后"两段,由 <>1__state 串联起来:

private sealed class <Main>d__0 : IAsyncStateMachine
{
    public int <>1__state;
    public AsyncTaskMethodBuilder <>t__builder;
    public string[] args;

    private Task<int> <res>5__1;
    private Task<int> <>s__2;
    private TaskAwaiter<Task<int>> <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        try
        {
            TaskAwaiter<Task<int>> awaiter;
            if (num != 0)
            {
                awaiter = Task.Factory.StartNew(() => Task.FromResult(0)).GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <Main>d__0 stateMachine = this;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter<Task<int>>);
                num = (<>1__state = -1);
            }
            <>s__2 = awaiter.GetResult();
            <res>5__1 = <>s__2;
            <>s__2 = null;
            Console.WriteLine(<res>5__1);
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <res>5__1 = null;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <res>5__1 = null;
        <>t__builder.SetResult();
    }
    // ...
}

MoveNext 不仅仅是"前进一步",它在整个生命周期里会被多次调用:第一次启动时调用一次,每次从 await 挂起恢复时再被调用一次。下面我们就跟着调用顺序,把这个过程走一遍。

二、状态机的工作流程

2.1 初始化

Main 改写后的代码可以看出,初始化分为四步:

  1. 创建状态机实例:new <Main>d__0(),生成一个继承 IAsyncStateMachine 的实例,用来承载所有上下文。
  2. 构建异步控制器:AsyncTaskMethodBuilder.Create() 创建驱动状态机的"调度器"。
  3. 设置初始状态:<>1__state = -1 标记这是首次执行。
  4. 启动状态机:调用 <>t__builder.Start(ref stateMachine),内部触发第一次 MoveNext

2.2 执行过程总览

Snipaste_2025-09-30_13-04-07.bmp

整个过程可以拆成五步,我们按调用顺序逐一展开。

2.3 第一次调用 MoveNext:启动任务,判断是否需要等待

AsyncTaskMethodBuilder.Start() 内部会立即调用一次 MoveNext,此时 <>1__state == -1,执行进入 if (num != 0) 分支。

这一步会产生两种可能的结果:

  1. Task 同步完成:awaiter.IsCompleted == true,直接走到下面的 GetResult(),整个异步方法可以一口气跑完,不需要真正"异步"。比如 await Task.FromResult(0) 这类场景就属于这种情况。
  2. Task 未同步完成:awaiter.IsCompleted == false,需要把当前进度保存下来、注册回调、然后退出。本示例使用的是 Task.Factory.StartNew(...),几乎一定走的是这条路径。

代码逻辑(加上行内注释会更清晰):

private void MoveNext()
{
    int num = <>1__state;
    try
    {
        TaskAwaiter<Task<int>> awaiter;
        // 第一次进入:num == -1,走这个分支
        if (num != 0)
        {
            // 启动任务并取得对应的 awaiter
            awaiter = Task.Factory.StartNew(() => Task.FromResult(0)).GetAwaiter();

            // 任务还没完成,需要挂起
            if (!awaiter.IsCompleted)
            {
                num = (<>1__state = 0);                         // 记录"我挂起在第 0 号挂起点"
                <>u__1 = awaiter;                               // 保存 awaiter,以便恢复时取结果
                <Main>d__0 stateMachine = this;
                <>t__builder.AwaitUnsafeOnCompleted(            // 注册"任务完成后再回到 MoveNext"
                    ref awaiter, ref stateMachine);
                return;                                          // 当前线程到此返回,方法挂起
            }
        }
        else
        {
            // 恢复时才走这个分支(见第二次 MoveNext)
            awaiter = <>u__1;
            <>u__1 = default(TaskAwaiter<Task<int>>);
            num = (<>1__state = -1);
        }

        // 同步完成 或 恢复后:取出结果并执行 await 之后的代码
        <>s__2 = awaiter.GetResult();
        <res>5__1 = <>s__2;
        <>s__2 = null;
        Console.WriteLine(<res>5__1);
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <res>5__1 = null;
        <>t__builder.SetException(exception);
        return;
    }

    // 整个方法正常结束
    <>1__state = -2;
    <res>5__1 = null;
    <>t__builder.SetResult();
}

可以看到,MoveNext 巧妙地用一个 if/else 把"首次执行"和"恢复执行"两种情况合并在了同一段代码里,真正的"跳转"靠的就是 <>1__state 这个状态值。

2.4 挂起阶段:AwaitUnsafeOnCompleted 做了什么

接下来,AwaitUnsafeOnCompleted 会把"任务完成后该做什么"告诉这个 awaiter。它会根据 awaiter 的具体类型走不同的分支,对 TaskAwaiter 来说,最终会调用到 TaskAwaiter.UnsafeOnCompletedInternal:

internal static void AwaitUnsafeOnCompleted<TAwaiter>(
    ref TAwaiter awaiter, IAsyncStateMachineBox box)
    where TAwaiter : ICriticalNotifyCompletion
{
    // 当 awaiter 是 TaskAwaiter 时进入此分支
    if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
    {
        ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter);
        // 第三个参数 continueOnCapturedContext: true,表示需要回到原同步上下文
        TaskAwaiter.UnsafeOnCompletedInternal(
            ta.m_task, box, continueOnCapturedContext: true);
    }
    // ...
}

注意这里的 continueOnCapturedContext: true,它就是 ConfigureAwait 的开关——3.2 节会回头讨论。

2.5 把 MoveNext 注册成 Task 的延续

UnsafeOnCompletedInternal 最终会调用 Task.UnsafeSetContinuationForAwait,把状态机(包含它的 MoveNext)作为延续(continuation)挂到目标 Task 上。任务完成时,运行时就知道该执行哪个回调:

internal void UnsafeSetContinuationForAwait(
    IAsyncStateMachineBox stateMachineBox, bool continueOnCapturedContext)
{
    TaskContinuation? tc;

    // 如果需要回到原上下文,优先尝试捕获 SynchronizationContext 或 TaskScheduler
    if (continueOnCapturedContext)
    {
        if (SynchronizationContext.Current is SynchronizationContext syncCtx
            && syncCtx.GetType() != typeof(SynchronizationContext))
        {
            tc = new SynchronizationContextAwaitTaskContinuation(
                syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false);
            goto HaveTaskContinuation;
        }

        if (TaskScheduler.InternalCurrent is TaskScheduler scheduler
            && scheduler != TaskScheduler.Default)
        {
            tc = new TaskSchedulerAwaitTaskContinuation(
                scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false);
            goto HaveTaskContinuation;
        }
    }

    // 没有需要捕获的上下文时,把状态机本身作为延续直接挂上去
    if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false))
    {
        // 任务已经完成,直接丢到线程池里执行
        ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal: true);
    }
    return;

HaveTaskContinuation:
    if (!AddTaskContinuation(tc, addBeforeOthers: false))
    {
        tc.Run(this, canInlineContinuationTask: false);
    }
}

简单总结一下这一步:状态机不是被"暂停"在某个线程上,而是把自己挂在了 Task 的"完成回调列表"里,然后当前线程立即返回。等到 Task 真正完成,运行时再把它从列表里取出来执行——这才是 async/await "释放线程"的本质。

2.6 任务完成时:触发延续

当目标 Task 完成时(无论成功、失败还是取消),运行时会调用 Task.FinishContinuationsRunContinuations 来跑挂在它身上的回调。对我们的状态机而言,匹配的是 IAsyncStateMachineBox 分支:

private void RunContinuations(object continuationObject)
{
    bool canInlineContinuations =
        (m_stateFlags & (int)TaskCreationOptions.RunContinuationsAsynchronously) == 0 &&
        RuntimeHelpers.TryEnsureSufficientExecutionStack();

    switch (continuationObject)
    {
        // 我们的状态机走这里
        case IAsyncStateMachineBox stateMachineBox:
            AwaitTaskContinuation.RunOrScheduleAction(stateMachineBox, canInlineContinuations);
            LogFinishCompletionNotification();
            return;

        case Action action:
            AwaitTaskContinuation.RunOrScheduleAction(action, canInlineContinuations);
            LogFinishCompletionNotification();
            return;

        case TaskContinuation tc:
            tc.Run(this, canInlineContinuations);
            LogFinishCompletionNotification();
            return;

        case ITaskCompletionAction completionAction:
            RunOrQueueCompletionAction(completionAction, canInlineContinuations);
            LogFinishCompletionNotification();
            return;
    }

    // 多个延续的情况省略...
}

这一步最终的效果就是:再次调用状态机的 MoveNext 方法

2.7 第二次调用 MoveNext:取出结果,执行剩余代码

第二次进入 MoveNext 时,<>1__state == 0,执行流走的是 else 分支:

private void MoveNext()
{
    int num = <>1__state;
    try
    {
        TaskAwaiter<Task<int>> awaiter;
        if (num != 0)
        {
            // 首次执行的代码,这次不再走
            // ...
        }
        else
        {
            // 第二次:num == 0,走这里
            awaiter = <>u__1;                          // 拿回挂起前保存的 awaiter
            <>u__1 = default(TaskAwaiter<Task<int>>);
            num = (<>1__state = -1);                   // 状态复位
        }

        // 取出任务结果
        <>s__2 = awaiter.GetResult();
        <res>5__1 = <>s__2;
        <>s__2 = null;

        // 执行 await 之后的代码
        Console.WriteLine(<res>5__1);
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <res>5__1 = null;
        <>t__builder.SetException(exception);
        return;
    }

    // 标记完成,设置最终返回的 Task 为成功
    <>1__state = -2;
    <res>5__1 = null;
    <>t__builder.SetResult();
}

到此为止,整个异步方法就跑完了。最后 <>t__builder.SetResult() 会把 Main 返回的那个 Task 标记为完成,等待它的调用者(如果有)就可以继续往下走。

2.8 小结

至此可以把整个过程概括成一句话:

await 不是"等",而是"先走开,完事再叫我回来"——编译器把方法切成几段,用 <>1__state 串联,通过把 MoveNext 注册成 Task 的延续来实现"挂起/恢复"。

三、同步上下文的捕获与 ConfigureAwait(false)

回到 2.4、2.5 节,你会发现 continueOnCapturedContext 这个参数贯穿了整个挂起流程。它就是控制"恢复时是否回到原同步上下文"的开关,而我们平时写的 .ConfigureAwait(false) 影响的正是这个开关。

3.1 默认情况:捕获并恢复上下文

不加任何配置时,await Task 走的是 TaskAwaiter,AwaitUnsafeOnCompleted 传入 continueOnCapturedContext: true:

internal static void AwaitUnsafeOnCompleted<TAwaiter>(
    ref TAwaiter awaiter, IAsyncStateMachineBox box)
    where TAwaiter : ICriticalNotifyCompletion
{
    if ((null != (object?)default(TAwaiter)) && (awaiter is ITaskAwaiter))
    {
        ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter);
        TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
    }
}

UnsafeSetContinuationForAwait 中,这个 true 会让代码优先检查 SynchronizationContext.CurrentTaskScheduler.InternalCurrent,只要其中有一个不是默认值,就用对应的延续把 MoveNext 包起来,确保恢复时回到原来的上下文(比如 WinForms 的 UI 线程、ASP.NET 的请求上下文):

if (continueOnCapturedContext)
{
    if (SynchronizationContext.Current is SynchronizationContext syncCtx
        && syncCtx.GetType() != typeof(SynchronizationContext))
    {
        tc = new SynchronizationContextAwaitTaskContinuation(
            syncCtx, stateMachineBox.MoveNextAction, flowExecutionContext: false);
        goto HaveTaskContinuation;
    }

    if (TaskScheduler.InternalCurrent is TaskScheduler scheduler
        && scheduler != TaskScheduler.Default)
    {
        tc = new TaskSchedulerAwaitTaskContinuation(
            scheduler, stateMachineBox.MoveNextAction, flowExecutionContext: false);
        goto HaveTaskContinuation;
    }
}

这就是为什么 UI 线程上 await 之后能直接操作控件——运行时帮你切回去了。

3.2 加上 ConfigureAwait(false):不切回原上下文

写成 await SomeTaskAsync().ConfigureAwait(false) 后,await 接受的对象就不再是 TaskAwaiter,而是 ConfiguredTaskAwaitable.ConfiguredTaskAwaiterAwaitUnsafeOnCompleted 走的是另一个分支:

else if ((null != (object?)default(TAwaiter)) && (awaiter is IConfiguredTaskAwaiter))
{
    ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta =
        ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
    TaskAwaiter.UnsafeOnCompletedInternal(
        ta.m_task,
        box,
        (ta.m_options & ConfigureAwaitOptions.ContinueOnCapturedContext) != 0);
}

注意最后一个参数:当 ConfigureAwait(false)ContinueOnCapturedContext 位为 0,传入的就是 false。于是 UnsafeSetContinuationForAwait 跳过捕获上下文的代码,直接把状态机挂到默认的延续列表里:

if (!AddTaskContinuation(stateMachineBox, addBeforeOthers: false))
{
    ThreadPool.UnsafeQueueUserWorkItemInternal(stateMachineBox, preferLocal: true);
}
return;

结果:await 之后的代码可能跑在任意一个线程池线程上,不会回到原来的 UI 线程或请求上下文。库代码里大量使用 ConfigureAwait(false) 的根本原因就在这里——它能避免无谓的上下文切换,以及更重要的:防止在持锁等待时与同步上下文产生死锁。

四、同步等待 Task 的两种方式

了解了 MoveNext 的恢复机制,再回头看"同步等待 Task 结果"的两种常见写法,差异就很清晰了。

4.1 Task.Result

  • 任务未完成时,同步阻塞当前线程直到完成。
  • 返回任务的结果(Task<TResult> 的情形)。
  • 异常处理:抛出 AggregateException,真正的异常被包装在 InnerExceptions 里。

4.2 Task.GetAwaiter().GetResult()

  • 同样会同步阻塞当前线程,直到任务完成。
  • 同样返回任务的结果。
  • 异常处理:直接抛出任务的原始异常,不做 AggregateException 包装。

4.3 该选哪个

  • 在不需要专门处理 AggregateException 的场景下,优先使用 Task.GetAwaiter().GetResult()——异常更直观,堆栈更干净。
  • 但更重要的是:这两种写法都会阻塞当前线程。如果在 UI 线程或同步上下文敏感的场景下使用,极易出现死锁(经典的"async 上下层混用导致 UI 卡死"问题)。
  • 因此,只要可能,就用 await 异步等待,把 .Result.GetAwaiter().GetResult() 留给那些确实无法引入 async 的边界场景(比如某些必须同步签名的入口)。

写在最后

回头看,编译器为一行 await 做的事情其实非常工整:

  1. 把方法拆成"挂起前"和"恢复后"两段,合并进 MoveNext
  2. <>1__state 这个整数当跳转表,串起所有挂起点。
  3. 通过 AsyncTaskMethodBuilder 协调状态机的启动、结果设置和异常传播。
  4. 通过 Task 的延续机制实现"任务完成后回到这里继续"。
  5. 通过 continueOnCapturedContext 决定恢复时是否切回原同步上下文。

理解了这套机制后,很多日常问题就有了清晰的答案:为什么 await 之后还能在 UI 线程上操作控件、为什么 ConfigureAwait(false) 能避免死锁、为什么 .Result 在 UI 线程上是个雷……它们都不再是孤立的"经验法则",而是同一套底层模型在不同场景下的自然推论。

评论