2.8 async 与 await
async / await 是 .NET 全栈开发中处理 IO 的基础能力。Web API、数据库、HTTP 调用、文件读写都应该优先使用异步方式,避免线程被长时间阻塞。
学习目标
- 理解异步不是多线程,而是释放等待 IO 的线程。
- 能正确返回
Task、Task<T>和ValueTask<T>。 - 能把
CancellationToken传递到数据库和 HTTP 调用。 - 能识别常见死锁、吞异常和并发误用。
应用场景
- ASP.NET Core API 调用数据库。
- 后端调用第三方 HTTP 服务。
- 批量处理文件、消息队列或后台任务。
- 前端提交后等待后端返回结果。
核心概念
| 概念 | 说明 |
|---|---|
Task | 表示一个还未完成或已经完成的异步操作 |
await | 异步等待结果,不阻塞当前线程 |
CancellationToken | 取消请求或超时操作的协作信号 |
Task.WhenAll | 并发等待多个互不依赖的异步操作 |
案例:异步查询任务
app.MapGet("/todos/{id:int}", async (
int id,
AppDbContext dbContext,
CancellationToken cancellationToken) =>
{
var todo = await dbContext.Todos
.AsNoTracking()
.Where(item => item.Id == id)
.Select(item => new TodoDto(item.Id, item.Title, item.IsCompleted))
.SingleOrDefaultAsync(cancellationToken);
return todo is null ? Results.NotFound() : Results.Ok(todo);
});
示例:并发调用互不依赖服务
public async Task<DashboardDto> GetDashboardAsync(
int userId,
CancellationToken cancellationToken)
{
var todosTask = todoService.GetTodayTodosAsync(userId, cancellationToken);
var profileTask = userService.GetProfileAsync(userId, cancellationToken);
await Task.WhenAll(todosTask, profileTask);
return new DashboardDto(
await todosTask,
await profileTask);
}
只有两个调用互不依赖时才适合并发;如果第二个调用需要第一个调用结果,就应顺序 await。
重点难点
- 不要用
.Result或.Wait()阻塞异步代码。 - Web API 中不要忽略
CancellationToken,客户端断开时应尽快停止无意义工作。 async void只适合少数事件处理,业务代码应返回Task。- 并发不是越多越好,数据库和第三方服务都有连接和速率限制。
常见误区
| 误区 | 推荐做法 |
|---|---|
认为 await 会新建线程 | 理解它是异步等待,不等于开线程 |
为了“更快”到处 Task.Run | IO 操作直接使用框架异步 API |
| 捕获异常后只写日志不抛出 | 明确返回失败结果或继续抛给统一错误处理 |
练习
- 把同步 EF Core 查询改为异步查询。
- 给一个 API 传入
CancellationToken,并传递到所有异步调用。 - 使用
Task.WhenAll并发加载用户资料和任务统计。