从反编译视角看 C# async/await 的工作原理
async 和 await 是 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 改写后的代码可以看出,初始化分为四步:
- 创建状态机实例:
new <Main>d__0(),生成一个继承IAsyncStateMachine的实例,用来承载所有上下文。 - 构建异步控制器:
AsyncTaskMethodBuilder.Create()创建驱动状态机的"调度器"。 - 设置初始状态:
<>1__state = -1标记这是首次执行。 - 启动状态机:调用
<>t__builder.Start(ref stateMachine),内部触发第一次MoveNext。
2.2 执行过程总览

整个过程可以拆成五步,我们按调用顺序逐一展开。
2.3 第一次调用 MoveNext:启动任务,判断是否需要等待
AsyncTaskMethodBuilder.Start() 内部会立即调用一次 MoveNext,此时 <>1__state == -1,执行进入 if (num != 0) 分支。
这一步会产生两种可能的结果:
- Task 同步完成:
awaiter.IsCompleted == true,直接走到下面的GetResult(),整个异步方法可以一口气跑完,不需要真正"异步"。比如await Task.FromResult(0)这类场景就属于这种情况。 - 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.FinishContinuations → RunContinuations 来跑挂在它身上的回调。对我们的状态机而言,匹配的是 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.Current 和 TaskScheduler.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.ConfiguredTaskAwaiter。AwaitUnsafeOnCompleted 走的是另一个分支:
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 做的事情其实非常工整:
- 把方法拆成"挂起前"和"恢复后"两段,合并进
MoveNext。 - 用
<>1__state这个整数当跳转表,串起所有挂起点。 - 通过
AsyncTaskMethodBuilder协调状态机的启动、结果设置和异常传播。 - 通过
Task的延续机制实现"任务完成后回到这里继续"。 - 通过
continueOnCapturedContext决定恢复时是否切回原同步上下文。
理解了这套机制后,很多日常问题就有了清晰的答案:为什么 await 之后还能在 UI 线程上操作控件、为什么 ConfigureAwait(false) 能避免死锁、为什么 .Result 在 UI 线程上是个雷……它们都不再是孤立的"经验法则",而是同一套底层模型在不同场景下的自然推论。