Runtime Async V2 在 .NET 11 中仍是预览功能。本文基于 .NET 11 Preview 6 和当前设计草案,后续版本的配置、限制与实现细节仍可能变化。

1. 相同的写法,不同的实现

.NET 11 前后,业务代码中的 asyncawait 基本不变:

static async Task<User> GetUserAsync(
    int id,
    CancellationToken cancellationToken)
{
    User user = await LoadUserAsync(id, cancellationToken);
    return user;
}

变化发生在编译和执行阶段:

  • .NET 11 之前,C# 编译器把异步方法改写为状态机。
  • 启用 Runtime Async V2 后,编译器生成运行时可识别的异步 IL,由运行时和 JIT 管理挂起与恢复。

这不是新的异步 API,也不需要重写现有的 await 调用,而是异步方法底层实现模型的变化。

2. .NET 11 之前:编译器生成状态机

传统异步方法会被编译器拆成入口方法和状态机。状态机通常包含:

  • 表示当前执行位置的状态字段。
  • 用于完成 Task 的异步方法构建器。
  • 需要跨越 await 保存的参数、局部变量和 awaiter。
  • 负责继续执行方法的 MoveNext()

下面是简化后的概念代码,不是编译器生成结果的逐行还原:

struct GetUserStateMachine : IAsyncStateMachine
{
    int state;
    TaskAwaiter<User> awaiter;
    AsyncTaskMethodBuilder<User> builder;

    public void MoveNext()
    {
        // 根据 state 恢复到对应位置,
        // 未完成时注册 continuation,完成后设置结果或异常。
    }
}

这种改写让 C# 很早就能支持完整的异步语义,但运行时看到的主要是普通方法、状态机和 continuation,难以直接理解原始异步方法的完整结构。

3. .NET 11:Runtime Async V2

Runtime Async V2 把异步方法提升为运行时能够识别的执行模型。编译器不再为这类方法生成传统的状态机类型,而是在 IL 中标记异步方法和挂起点,由运行时负责:

  • 在任务未完成时挂起当前方法。
  • 保存恢复执行真正需要的状态。
  • 在任务完成后从对应位置继续执行。
  • 为 JIT、ReadyToRun 和 NativeAOT 提供统一的异步代码生成能力。

从调用者角度看,方法仍返回 TaskValueTask;从运行时角度看,它不再只是一个包装状态机的普通方法。

4. 两种实现的主要差异

对比项.NET 11 之前.NET 11 Runtime Async V2
实现主体C# 编译器改写运行时与 JIT 管理
挂起与恢复状态机、MoveNext()、方法构建器运行时识别异步方法和挂起点
生成类型通常生成专用状态机类型不再生成传统编译器状态机
实时调用栈容易出现状态机和构建器帧更接近源码中的真实调用链
优化范围编译器围绕状态机优化JIT、ReadyToRun、NativeAOT 可直接理解异步语义
成熟度已长期用于生产环境.NET 11 中仍为预览功能

5. 性能与诊断变化

5.1 更清晰的实时调用栈

传统实现会在实时调用栈中暴露状态机和 AsyncMethodBuilder 等基础设施。Runtime Async 可以直接保留原始异步方法的调用关系,更方便分析 profiler、调试器调用栈以及 new StackTrace() 的结果。

官方示例中,实时调用栈从 13 帧减少到 5 帧。这个数字只对应示例代码,实际结果取决于调用层级和优化情况。

异常对象中的 stack trace 原本就会经过异步栈整理,因此两种实现下可能看起来相近。Runtime Async 改善的重点是程序运行期间看到的实时调用栈

5.2 减少异步基础设施开销

.NET 11 当前实现包含以下优化:

  • 更积极地缓存和复用 continuation。
  • 避免保存没有发生变化的局部状态。
  • 合并多个挂起点的公共代码,减小生成代码体积。
  • 为同步返回任务的方法生成专用的 runtime-async 版本,减少额外转发。
  • 支持 ReadyToRun 和 NativeAOT。
  • 允许没有实际挂起的同步快速路径被内联。

这些能力为降低分配、代码体积和调度开销提供了空间,但不代表每个异步方法都会更快。任务是否同步完成、挂起次数、ExecutionContext、I/O 延迟和调用频率都会影响结果。

5.3 不要混淆 ExecutionContext 优化

.NET 11 还会在没有环境状态需要保留时,跳过 continuation 的 ExecutionContext 捕获和恢复。TaskTask<T>ValueTaskValueTask<T> 都能受益。

这项优化会帮助 Runtime Async 路径,但它属于 .NET 11 的通用异步优化,不是只有开启 Runtime Async 才能获得的能力。

6. 启用与关闭

在 .NET 11 项目中显式启用:

<PropertyGroup>
  <TargetFramework>net11.0</TargetFramework>
  <Features>runtime-async=on</Features>
</PropertyGroup>

net11.0 项目不再需要额外设置:

<EnablePreviewFeatures>true</EnablePreviewFeatures>

.NET 11 运行时库本身已经使用 runtime-async=on 编译。需要按项目关闭时,使用:

<PropertyGroup>
  <UseRuntimeAsync>false</UseRuntimeAsync>
</PropertyGroup>

此前用于控制 Runtime Async 的 DOTNET_RuntimeAsyncUNSUPPORTED_RuntimeAsync 环境变量已经移除,不应继续依赖。

7. 支持范围与当前限制

Runtime Async 当前支持 TaskTask<T>ValueTaskValueTask<T> 四类返回类型。

当前规范仍是草案,包含一些 IL 和状态保存限制,例如:

  • by-ref 局部变量不能跨挂起点保存。
  • 挂起点不能位于异常处理块中。
  • 暂时禁止使用 localloc 指令和 tail 前缀。

这些是运行时、编译器和底层工具需要处理的约束。普通 C# 业务代码通常不需要直接操作相关 IL,但使用 IL weaving、自定义编译器、代码生成器或底层诊断工具时应重点验证兼容性。

8. 是否应该立即迁移

现有代码不需要为了 Runtime Async 重写 asyncawait。更合适的验证方式是:

  1. 在 .NET 11 测试分支中开启 runtime-async=on
  2. 运行单元测试、集成测试以及取消、异常和超时场景。
  3. 对异步热路径比较吞吐量、延迟、分配量和程序集体积。
  4. 检查 profiler、调试器、日志和 IL 工具是否正常工作。
  5. 如果收益不稳定或工具链不兼容,使用 UseRuntimeAsync 暂时关闭。

Runtime Async V2 的意义不只是少生成一个状态机类型,而是让运行时第一次能够直接理解和优化异步方法。它为后续优化打开了更大的空间,但在预览阶段,是否采用仍应由真实负载和兼容性测试决定。

9. 官方资料