第 5 章:async/await 的本质:状态机

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

揭示编译器如何将 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 Taskasync 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 面试要点

  1. async/await 的本质是什么?

    • 编译器将 async 方法转换为状态机,每个 await 是一个挂起点
  2. async/await 会创建新线程吗?

    • 不会。它释放当前线程,I/O 完成后在线程池线程上继续
  3. ConfigureAwait(false) 的作用?

    • 不捕获 SynchronizationContext,await 后在线程池线程上执行
    • 避免死锁,提高性能
  4. async void 的问题?

    • 不能被 await,异常无法被捕获,可能导致进程崩溃
  5. 什么是 async 死锁?如何避免?

    • 同步等待异步结果(.Result/.Wait())+ SynchronizationContext
    • 解决:async 全链路 或 ConfigureAwait(false)

练习

  1. 使用 ILSpy/dnSpy 反编译一个 async 方法,观察生成的状态机
  2. 在 WPF 中复现 async 死锁,然后用 ConfigureAwait(false) 修复
  3. 对比 async Task 和直接返回 Task 的性能差异
  4. 使用 ValueTask 优化频繁调用的异步方法

← 上一章:垃圾回收深入剖析 | 下一章:Task 与线程池深入 →

评论

登录 后发表评论

暂无评论