c# Stephen Cleary 关于异步编程的建议 c# async expert

来源:这里教程网 时间:2026-02-21 17:36:00 作者:

Stephen Cleary 是 C# 异步编程领域公认的实战派专家,他不是只讲理论的学者,而是长期在真实项目中踩坑、填坑、提炼模式的工程师。他的核心建议不是“怎么写 async”,而是“怎么不写错 async”——尤其在跨线程上下文、测试、UI 集成和库设计等容易静默崩溃的场景。

避免
async void
,除非你真在写事件处理程序

这是 Cleary 反复强调的“第一戒律”。

async void
方法无法被
await
,异常会直接抛到
SynchronizationContext
(比如 UI 线程),导致应用崩溃且难以捕获;它也不支持超时、取消或组合(如
Task.WhenAll
)。

✅ 正确:事件处理器(如
button_Click
)可
async void
,因为事件签名强制返回
void
❌ 错误:把业务逻辑方法写成
async void DoWork()
—— 改为
async Task DoWorkAsync()
⚠️ 注意:
Console.Main
在 .NET 6+ 允许
async Task Main()
,但旧版或类库中仍需避免
void

ConfigureAwait(false)
除非你明确需要上下文

默认情况下,

await
会尝试“回到原上下文”(如 UI 线程或 ASP.NET 请求上下文)。这在库代码中是危险的:它可能引发死锁(尤其调用
.Result
.Wait()
时),也拖慢性能。

✅ 库/工具方法中:所有
await
后加
.ConfigureAwait(false)
,例如
await stream.ReadAsync(buffer).ConfigureAwait(false)
❌ UI 层或 ASP.NET Controller 中盲目加:可能破坏数据绑定或
HttpContext
访问
? 小技巧:用 Roslyn 分析器(如
ConfigureAwaitChecker
)自动检测遗漏点

测试异步方法时,别用
.Result
.Wait()

Cleary 明确指出:“单元测试里出现

.Result
,基本等于埋雷”。它会阻塞当前线程,在 xUnit/NUnit 的同步测试上下文中极易触发死锁(尤其涉及
SynchronizationContext
时)。

✅ 正确:测试方法本身标记为
async Task
,并
await
被测方法
❌ 错误:
[Fact]
public void TestGetData() {
    var result = GetDataAsync().Result; // ⚠️ 死锁高发区
    Assert.Equal("OK", result);
}
? 补充:xUnit 支持
async Task
测试方法;NUnit 需 ≥ 3.0 并启用
AsyncTest
特性

别让 CPU 密集型操作假装“异步”

异步 ≠ 并行。Cleary 强调:

await Task.Run(() => ComputeHeavy())
不是真正的异步 I/O,只是把同步工作扔进线程池——它解决的是 UI 响应问题,但会增加调度开销,且不能缩放。

✅ I/O 绑定操作(HTTP、DB、文件):用原生异步 API(
HttpClient.GetAsync
FileStream.ReadAsync
✅ CPU 绑定操作:用
Parallel.ForEach
PLINQ
,或明确用
Task.Run
并注明“此为线程池卸载,非异步”
❌ 混淆:
async Task<int> CalculateAsync() { await Task.Run(() => ExpensiveMath()); }</int>
—— 方法名带
Async
却无真正异步语义,误导调用方

真正难的从来不是“怎么让代码跑起来”,而是“怎么让它在并发、中断、失败、升级后依然可靠”。Cleary 的建议之所以被广泛采纳,是因为他始终站在调用链下游、测试覆盖率、部署稳定性这些真实战场说话——比如一个

ConfigureAwait(false)
的缺失,可能在压测时才暴露为 5% 的随机超时,而没人能复现。

相关推荐