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 实战:避免阻塞与常见并发陷阱的关键在于把隐含假设变成可执行的约束,并通过测试、监控和复盘持续验证。先从影响最大的真实场景开始,小步调整并保留回滚能力,通常比一次性大范围改造更安全,也更容易积累可复用的工程经验。