async/await 不是免费午餐:避免死锁与线程池拥堵的 5 个习惯

作者:微信公众号:【架构师老卢】
2-1 10:20
45

这篇文章不讲“语法”,讲的是线上更稳的 async/await 使用习惯。

1) 不要用 .Result / .Wait() 阻塞等待

阻塞等待会把“异步”硬拉回同步世界:

  • 在有同步上下文(老版 ASP.NET、WinForms/WPF)里可能引发 死锁
  • 在 ASP.NET Core 里更常见的是 线程池被占满,吞吐掉到谷底

正确姿势:一路 async 到底

var data = await client.GetStringAsync(url);

2) 不要把 I/O 异步工作塞进 Task.Run

Task.Run 适合把 CPU 密集型 工作丢到线程池; 但如果你本来就是 I/O 异步(HTTP/DB/文件),再包一层 Task.Run 只会:

  • 额外占用线程池线程
  • 增加上下文切换
  • 更难排查问题

3) 认真传递并观察 CancellationToken

取消是 .NET 里“优雅停机”和“及时释放资源”的基础能力。

  • API 入参有 CancellationToken 就要传下去
  • 循环/流式处理时要定期检查取消
public async Task<string> FetchAsync(HttpClient http, string url, CancellationToken ct)
    => await http.GetStringAsync(url, ct);

4) 别“fire-and-forget”吞掉异常

_ = SomeAsync(); 这种写法如果没有可靠的异常处理/日志,失败会变成“沉默失败”。

如果你确实需要后台执行:

  • BackgroundService / 队列
  • 统一 try/catch + 记录日志
  • 可选:度量(metrics)+ 告警

5) 区分“并发”与“串行”,用对工具

  • 串行:foreach + await
  • 并发:Task.WhenAll
  • 限流并发:SemaphoreSlim / Channel
var tasks = urls.Select(u => http.GetStringAsync(u, ct));
var pages = await Task.WhenAll(tasks);

参考链接

  • Task-based asynchronous pattern (TAP):https://learn.microsoft.com/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap
  • async/await 概览:https://learn.microsoft.com/dotnet/csharp/asynchronous-programming/
相关留言评论
昵称:
邮箱:
阅读排行