C# async/await、Task 、死锁

一、核心

 
  • 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 方法编译成一个状态机类,结构类似:
 
  1. 走到 await
  2. 保存当前方法上下文(变量、位置)
  3. 挂起方法,返回调用方
  4. 线程释放,去干别的
  5. 异步操作完成
  6. 从线程池取线程,恢复上下文,继续执行后续代码
 
关键点:
 
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;
}
 

为什么死锁?

 
  1. 主线程被 Wait () 阻塞
  2. await 完成后,想回到主线程上下文继续执行
  3. 但主线程已经卡住,在等 Task 完成
  4. 互相等待 → 死锁
 

根本原因

 
默认情况下:
 
await 会尝试恢复到原 SynchronizationContext
 
  • UI:UI 线程
  • ASP.NET:请求上下文
  • Console / ASP.NET Core:无上下文,不会死锁
 

 

六、解决死锁的方案

 

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 异步真正流程

 
  1. 调用 await ReadAsync
  2. 线程发出指令给操作系统
  3. 线程回到线程池
  4. 磁盘 / 网络完成,发中断
  5. 线程池取出一个线程
  6. 恢复 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 高并发关键:不占线程等待
posted @ 2026-04-24 08:35  好好校习DayDayUp  阅读(66)  评论(0)    收藏  举报