一、核心
- Task:代表一个尚未完成的操作(可以是异步、也可以是同步)
- async/await:语法糖,让异步代码写得像同步
- 本质:await 时挂起方法,释放线程;操作完成后恢复执行
二、Task 到底是什么?
1. Task 不是线程
很多人误区:
“启动一个 Task 就开一个线程。”
错。
- Task 是操作的承诺(Promise)
- 它只表示 “这件事未来会完成”
- 不代表一定用新线程
2. Task 分两类
(1)CPU 密集型
真正开线程 / 线程池
Task.Run(() => {
// 计算密集逻辑
});
(2)IO 密集型
网络请求、文件读写、数据库、延时…… 不占线程! 内核级异步,线程直接释放,等硬件中断回来。
await httpClient.GetAsync(url);
await File.ReadAllTextAsync(path);
await Task.Delay(1000);
3. Task 状态机
一个 Task 有:
RanToCompletion成功Faulted异常Canceled取消IsCompleted是否完成
4. Task 为什么能 “等待”?
因为它实现了
GetAwaiter() 只要一个对象有这个方法,就能被 await。
三、async/await 原理
编译器会把 async 方法编译成一个状态机类,结构类似:
- 走到 await
- 保存当前方法上下文(变量、位置)
- 挂起方法,返回调用方
- 线程释放,去干别的
- 异步操作完成
- 从线程池取线程,恢复上下文,继续执行后续代码
关键点: await 之后的代码,不一定在原来线程上执行!
四、async/await 用法
1. 标准写法
async Task<int> GetDataAsync()
{
await Task.Delay(100); // 异步等待
return 100;
}
2. 无返回值
async Task WorkAsync() { ... }
不要用
async void!除非是事件处理。
3. 等待多个任务
await Task.WhenAll(t1, t2, t3);
4. 任一完成就继续
await Task.WhenAny(t1, t2);
5. 同步等待(危险)
task.Wait();
var result = task.Result;
这是死锁重灾区。
五、异步死锁 99% 场景:上下文争夺
经典死锁代码(WinForm / WPF / ASP.NET(非 Core)必现)
// UI线程或ASP.NET主线程
void Button_Click()
{
var t = GetDataAsync();
t.Wait(); // 阻塞主线程
}
async Task<int> GetDataAsync()
{
await Task.Delay(100); // 想切回原上下文
return 1;
}
为什么死锁?
- 主线程被 Wait () 阻塞
- await 完成后,想回到主线程上下文继续执行
- 但主线程已经卡住,在等 Task 完成
- 互相等待 → 死锁
根本原因
默认情况下: await 会尝试恢复到原 SynchronizationContext
六、解决死锁的方案
1. 推荐全程 async/await,不阻塞
async void Button_Click()
{
await GetDataAsync();
}
2. 必须同步等待时:
task.ConfigureAwait(false).GetAwaiter().GetResult();
3. 库代码加
await SomeTask().ConfigureAwait(false);
含义: 不需要恢复到原来的上下文,随便找个线程池线程继续。 这是杜绝死锁的最关键习惯。
七、async/await 常见坑
1. async void 灾难
除了事件,永远不要写 async void
- 异常抓不到
- 无法等待
- 无法取消
- 无法处理异常
2. 忘记 await
DoWorkAsync(); // 直接调用,不等待
变成 “火并忘”(fire and forget) 异常直接吞,程序莫名崩。
3. 重复 await
var t = MethodAsync();
await t;
await t; // 无害,但多余
4. Task.Run 包裹本来就异步的方法
await Task.Run(async ()=> await httpClient.GetAsync(...));
毫无意义,浪费线程。
5. 用 Task.Delay 做循环轮询
可以,但要加取消令牌。
八、Task 原理进阶:线程去哪儿了?
IO 异步真正流程
- 调用
await ReadAsync - 线程发出指令给操作系统
- 线程回到线程池
- 磁盘 / 网络完成,发中断
- 线程池取出一个线程
- 恢复 async 方法,继续执行
一句话: 异步 IO 不阻塞线程,线程是被释放的,不是在等待。 这就是高并发关键。
九、实践
1. 方法名后缀 Async
GetDataAsync()
SaveAsync()
2. 库代码一律
await xxx.ConfigureAwait(false);
3. 不使用 .Result/.Wait () /.WaitAll ()
除非你非常清楚上下文机制。
4. 尽量返回 Task,不要 async void
事件除外。
5. 异常统一捕获
await 内部异常会正常抛出,直接 try/catch 即可。
6. 用 CancellationToken 做取消
await MethodAsync(cts.Token);
7. 不要在异步里锁(Monitor、lock)
极易死锁。
8. ASP.NET Core 全程异步
从控制器 → 服务 → 数据库全异步,吞吐量提升巨大。
十、总结
- Task 是操作的承诺,不是线程
- async/await 是状态机语法糖,实现挂起与恢复
- await 不阻塞线程,释放线程 → 完成后恢复
- 死锁根源:同步阻塞(Wait/Result)+ 上下文恢复
- 防死锁:全程异步 + ConfigureAwait (false)
- 异步 IO 高并发关键:不占线程等待
文章摘自:https://www.cnblogs.com/chuansheng/p/19908907
