C# 中的异步编程模型:虚拟线程与协程
C# 的 async/await 是一个通用的异步编程模型,编译器采用 CPS 变换将 async 方法拆分,并通过 await 作为分界线构建状态机来驱动执行。这种方式可以被看作是 stackless coroutine。
以以下代码为例:

经过编译后会变成类似如下形式(极度简化版):

C# 允许开发者自定义 Scheduler,尽管 .NET 默认使用线程池作为调度器,但也可以设计基于事件循环的单线程调度器,比如 JavaScript 的 Promise 模式。此外,.NET 还提供了 AsyncMethodBuilder,使得开发者能自行实现 Task 类型和状态机。
async/await 本身并不涉及具体的线程或调度逻辑,它是一个纯粹的编译器特性。在 C# 中,语言提供 Task 和线程池来支持一般性异步操作,同时也演化出了 ValueTask、PooledValueTask 等性能优化方案。
Stackless Coroutine 优势
- 不需要保存寄存器上下文,只需将局部变量提升到状态机闭包中;
- 占用内存极小,仅需几十字节;
- 遵循正常的分支判断和函数调用,有利于 CPU 控制流、分支预测和缓存表现。
Goroutine 的缺点
- 需要模拟系统线程并在用户态管理 stack,每个 goroutine 至少占用 8KB 堆栈空间;
- 频繁切换时上下文备份和恢复操作会引发严重的分支预测失败和缓存缺失问题;
- 虽对老代码兼容较好,但在无历史包袱的情况下几乎无优势。
C# 异步编程的优势与不足
async/await 提供了高度灵活性,既不绑定具体调度策略,也因无需维护虚拟线程堆栈而显著降低资源消耗。更重要的是,它对 CPU 控制流友好,从而提升了整体性能。
然而,其编写体验不如 Go 的 stackful 协程直观,需要主动重构代码并替换所有阻塞调用为 async/await 风格。
性能实测对比
某些网络流传的测试如 "Go vs C#, part 1: Goroutines vs Async-Await" 存在不合理之处。例如 C# 测试代码故意添加 await Task.YieldAsync() 导致额外开销,这等价于人为插入 Thread.Sleep(1) 来制造性能瓶颈。
即使在 .NET Core 1.1 上,加入无意义 yield 操作后,C# 依然能在升级至最新 .NET 7.0 后表现优于 Go。更关键的是,相比运行时需要数 GB 内存的 Go 实现,C# 版本只需要十几 MB 就可完成相同测试。

