C# async/await 实战:避免阻塞与常见并发陷阱
async/await 让异步 I/O 代码保持顺序阅读体验,但它并不会自动创建后台线程。可靠实现的重点是让异步贯穿调用链、避免同步阻塞,并为每项外部操作设置取消与超时。
核心原则
- 异步方法返回 Task 或 Task
,除事件处理器外避免 async void。 - 从控制器到数据访问保持 async all the way,不使用 .Result 或 .Wait。
- 把 CancellationToken 传给 HttpClient、EF Core 和其他下游 API。
- 独立任务可用 Task.WhenAll 并发执行,但必须根据下游容量限制数量。
- 在服务边界记录异常,保留原始堆栈,不使用 throw ex 重新抛出。
推荐的实践步骤
请求处理方法接收框架提供的 CancellationToken,并传给所有异步操作。多个相互独立的查询可以先创建任务再 WhenAll;批量外部请求则配合 SemaphoreSlim 限流。使用 CancelAfter 或 WaitAsync 设置总预算,并区分用户取消与系统超时。
public async Task LoadAsync(
int id, CancellationToken cancellationToken)
{
var user = await _db.Users
.AsNoTracking()
.SingleAsync(x => x.Id == id, cancellationToken);
return new UserView(user.Id, user.Name);
}
示例用于说明实现思路,实际项目还应结合所使用的框架版本、部署环境和业务约束进行调整。重要配置要进入版本管理,并在测试环境验证后再发布。
常见误区
- 在 ASP.NET Core 请求中调用 .Result,可能造成线程池饥饿并降低吞吐。
- 无界创建大量 Task 会同时压垮数据库连接池或外部服务。
- 忽略 CancellationToken 会让客户端断开后工作仍继续占用资源。
上线前检查清单
- 确认“异步方法返回 Task 或 Task
,除事件处理器外避免 async void”已经通过代码审查或运行验证。
- 确认“从控制器到数据访问保持 async all the way,不使用 .Result 或 .Wait”已经通过代码审查或运行验证。
- 确认“把 CancellationToken 传给 HttpClient、EF Core 和其他下游 API”已经通过代码审查或运行验证。
- 确认“独立任务可用 Task.WhenAll 并发执行,但必须根据下游容量限制数量”已经通过代码审查或运行验证。
- 为失败路径、边界条件和回滚方案准备测试或演练记录。
- 上线后观察错误率、延迟和资源消耗,确认变化符合预期。
总结
C# async/await 实战:避免阻塞与常见并发陷阱的关键在于把隐含假设变成可执行的约束,并通过测试、监控和复盘持续验证。先从影响最大的真实场景开始,小步调整并保留回滚能力,通常比一次性大范围改造更安全,也更容易积累可复用的工程经验。