第 5 章:async/await 的本质:状态机
jerry北京市2026年4月12日C# 8 次阅读 约 17 分钟

揭示编译器如何将 async 方法转换为状态机,理解 SynchronizationContext、ConfigureAwait 和异步的真正含义。
5.1 async/await 不是多线程
这是最常见的误解。async/await 的本质是异步,不是并行:
// 这个方法不会创建新线程
async Task<string> GetDataAsync()
{
// 发起 HTTP 请求后,当前线程被释放(不阻塞)
var response = await httpClient.GetStringAsync("https://api.example.com/data");
// 响应回来后,在某个线程上继续执行
return response;
}
异步的核心价值:在等待 I/O 操作时释放线程,让线程去做其他事情。
同步(阻塞):
Thread 1: [发起请求] ──等待──等待──等待── [处理响应]
线程被浪费
异步(非阻塞):
Thread 1: [发起请求] → 线程释放,去处理其他请求
Thread N: [处理响应] ← I/O 完成后,某个线程继续
5.2 编译器生成的状态机
编译器将 async 方法转换为一个状态机类。看一个简单的例子:
// 你写的代码
async Task<int> CalculateAsync()
{
int a = await GetValueAsync(1);
int b = await GetValueAsync(2);
return a + b;
}
编译器大致生成的代码(简化版):
// 编译器生成的状态机结构体
[AsyncStateMachine(typeof(CalculateAsyncStateMachine))]
Task<int> CalculateAsync()
{
var stateMachine = new CalculateAsyncStateMachine();
stateMachine._builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine._state = -1; // 初始状态
stateMachine._builder.Start(ref stateMachine);
return stateMachine._builder.Task;
}
struct CalculateAsyncStateMachine : IAsyncStateMachine
{
public int _state;
public AsyncTaskMethodBuilder<int> _builder;
// 局部变量提升为字段
private int _a;
private int _b;
private TaskAwaiter<int> _awaiter;
public void MoveNext()
{
int result;
try
{
switch (_state)
{
case -1: // 初始状态
_awaiter = GetValueAsync(1).GetAwaiter();
if (!_awaiter.IsCompleted)
{
_state = 0;
_builder.AwaitUnsafeOnCompleted(ref _awaiter, ref this);
return; // 挂起,等待完成后回调 MoveNext
}
goto case 0;
case 0: // 第一个 await 完成
_a = _awaiter.GetResult();
_awaiter = GetValueAsync(2).GetAwaiter();
if (!_awaiter.IsCompleted)
{
_state = 1;
_builder.AwaitUnsafeOnCompleted(ref _awaiter, ref this);
return;
}
goto case 1;
case 1: // 第二个 await 完成
_b = _awaiter.GetResult();
result = _a + _b;
break;
}
}
catch (Exception ex)
{
_state = -2;
_builder.SetException(ex);
return;
}
_state = -2; // 完成状态
_builder.SetResult(result);
}
}
关键点:
- 每个
await是一个挂起点,对应状态机的一个状态 - 局部变量被提升为状态机的字段(因为要跨 await 保持值)
MoveNext()在每次 await 完成后被回调- 如果 await 的 Task 已经完成(
IsCompleted = true),不会挂起,直接继续
5.3 SynchronizationContext
SynchronizationContext 决定了 await 之后的代码在哪个线程上执行:
// WPF/WinForms 中
async void Button_Click(object sender, EventArgs e)
{
// 在 UI 线程上
label.Text = "加载中...";
var data = await GetDataAsync();
// await 之后仍然在 UI 线程上(SynchronizationContext 的作用)
label.Text = data; // 可以安全地更新 UI
}
| 环境 | SynchronizationContext | await 后的线程 |
|---|---|---|
| WPF/WinForms | DispatcherSynchronizationContext | UI 线程 |
| ASP.NET Core | 无(null) | 线程池任意线程 |
| 控制台应用 | 无(null) | 线程池任意线程 |
5.4 ConfigureAwait
// ConfigureAwait(false):不捕获 SynchronizationContext
// await 之后在线程池线程上执行
var data = await GetDataAsync().ConfigureAwait(false);
// ConfigureAwait(true):默认行为,捕获 SynchronizationContext
var data = await GetDataAsync().ConfigureAwait(true);
使用建议:
- 库代码:始终使用
ConfigureAwait(false)(避免死锁,提高性能) - 应用代码(ASP.NET Core):不需要加(没有 SynchronizationContext)
- 应用代码(WPF/WinForms):需要更新 UI 时不加,不需要时加
false
经典死锁场景
// WPF/WinForms 中的死锁
void Button_Click(object sender, EventArgs e)
{
// .Result 阻塞 UI 线程
var data = GetDataAsync().Result; // 死锁!
}
async Task<string> GetDataAsync()
{
var data = await httpClient.GetStringAsync("...");
// await 完成后需要回到 UI 线程(SynchronizationContext)
// 但 UI 线程被 .Result 阻塞了 → 死锁
return data;
}
// 解决方案 1:使用 async/await 全链路
async void Button_Click(object sender, EventArgs e)
{
var data = await GetDataAsync(); // 不阻塞
}
// 解决方案 2:ConfigureAwait(false)
async Task<string> GetDataAsync()
{
var data = await httpClient.GetStringAsync("...").ConfigureAwait(false);
return data;
}
5.5 async void vs async Task
// async Task:可以被 await,异常可以被捕获
async Task DoWorkAsync()
{
throw new Exception("error");
}
try
{
await DoWorkAsync(); // 异常被正常捕获
}
catch (Exception ex) { }
// async void:不能被 await,异常会导致进程崩溃
async void FireAndForget()
{
throw new Exception("error"); // 未处理的异常 → 进程崩溃
}
规则:
- 永远使用
async Task或async Task<T> async void只用于事件处理器(如Button_Click)
5.6 常见的 async 反模式
// 反模式 1:async over sync(在 async 方法中包装同步代码)
async Task<int> BadAsync()
{
return await Task.Run(() => HeavyComputation()); // 浪费一个线程池线程
}
// 正确:如果是 CPU 密集型,让调用者决定是否用 Task.Run
// 反模式 2:sync over async(同步等待异步结果)
void Bad()
{
var result = GetDataAsync().Result; // 可能死锁
var result2 = GetDataAsync().GetAwaiter().GetResult(); // 同样可能死锁
}
// 正确:async 全链路
// 反模式 3:不必要的 async/await
async Task<int> Unnecessary()
{
return await GetValueAsync(); // 多了一层状态机
}
// 正确:直接返回 Task
Task<int> Better()
{
return GetValueAsync(); // 无额外开销
}
// 注意:如果有 try/catch 或 using,则需要 async/await
// 反模式 4:忘记 await
async Task Process()
{
GetDataAsync(); // 忘记 await!异常会被吞掉
// 应该:await GetDataAsync();
}
5.7 面试要点
-
async/await 的本质是什么?
- 编译器将 async 方法转换为状态机,每个 await 是一个挂起点
-
async/await 会创建新线程吗?
- 不会。它释放当前线程,I/O 完成后在线程池线程上继续
-
ConfigureAwait(false) 的作用?
- 不捕获 SynchronizationContext,await 后在线程池线程上执行
- 避免死锁,提高性能
-
async void 的问题?
- 不能被 await,异常无法被捕获,可能导致进程崩溃
-
什么是 async 死锁?如何避免?
- 同步等待异步结果(.Result/.Wait())+ SynchronizationContext
- 解决:async 全链路 或 ConfigureAwait(false)
练习
- 使用 ILSpy/dnSpy 反编译一个 async 方法,观察生成的状态机
- 在 WPF 中复现 async 死锁,然后用 ConfigureAwait(false) 修复
- 对比
async Task和直接返回Task的性能差异 - 使用
ValueTask优化频繁调用的异步方法
评论
登录 后发表评论
暂无评论