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 高并发关键:不占线程等待

文章摘自:https://www.cnblogs.com/chuansheng/p/19908907