这一章是 async体系里最容易被误用的知识点。
很多程序员写代码时:
await Task.Run(...)
但他们其实不知道为什么。
1 Task.Run 的真正作用
Task.Run 的作用只有一个:把工作丢到线程池执行
例如:
await Task.Run(() =>
{
DoCpuWork();
});
执行模型:
主线程
│
▼
Task.Run
│
▼
线程池线程
│
▼
HeavyCalculation
所以 Task.Run 的本质是:线程切换,而不是:异步IO。
2 为什么 IO 不需要 Task.Run
看这个代码:
await httpClient.GetStringAsync(url);
执行模型:
线程发送HTTP请求
↓
线程释放
↓
服务器响应
↓
线程池线程继续执行
注意:
整个过程没有线程阻塞
所以:
不需要 Task.Run
3 IO任务 vs CPU任务
这是 async 编程最核心的分类。
IO任务执行模型
线程发起IO
↓
线程释放
↓
IO完成
↓
线程继续执行
特点:
线程不会被占用
CPU任务执行模型
线程执行计算
↓
一直占用CPU
↓
完成
特点:
线程被长期占用
所以:
必须丢到线程池
4 UI程序为什么必须 Task.Run
例如:
WPF程序:
private async void Button_Click(...)
{
Calculate();
}
如果 Calculate() 很重:
UI卡死
正确写法:
private async void Button_Click(...)
{
await Task.Run(() => Calculate());
}
执行模型:
UI线程
↓
Task.Run
↓
线程池计算
↓
UI继续响应
5 服务器程序为什么很少用 Task.Run
例如:
ASP.NET:
public async Task<string> Get()
{
return await httpClient.GetStringAsync(url);
}
服务器的优势是:
IO等待时线程会释放
所以一个服务器:
几十个线程
处理几千请求
如果你乱用 Task.Run:
线程池爆炸
性能下降
6 一个经典错误
很多人写:
public async Task<string> Get()
{
return await Task.Run(async () =>
{
return await httpClient.GetStringAsync(url);
});
}
这相当于:
开线程
↓
线程等待IO
↓
完全浪费
工程里属于:严重反模式
7 async + CPU 的正确模式
如果既有 IO 又有 CPU:
正确写法:
var data = await httpClient.GetStringAsync(url);
var result = await Task.Run(() =>
{
return ProcessData(data);
});
执行模型:
IO异步
↓
CPU线程池
这是 最标准结构。
本章建立的核心模型
你以后只需要问自己一个问题:这是 IO 还是 CPU
如果是:IO
await IOOperation();
如果是:CPU
await Task.Run(() => CpuWork());
练习1(线程推导)
代码:
async Task Test()
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
问题:两个线程ID一定相同吗?为什么?
1 方法被挂起
await Task.Delay
↓
Test方法挂起
线程:不会等待
而是:返回线程池
2 继续执行的线程可能不同
执行流程:
线程1 执行 Test
↓
await Delay
↓
线程1 释放
1秒后
线程池找线程继续执行
可能是线程2
所以:线程ID 不一定相同
练习2(并发能力)
代码:
async Task Test()
{
var t1 = httpClient.GetStringAsync(url1);
var t2 = httpClient.GetStringAsync(url2);
var t3 = httpClient.GetStringAsync(url3);
await Task.WhenAll(t1, t2, t3);
}
问题:这里会不会创建3个线程?
正确答案是:不会创建3个线程
原因:HTTP请求是异步IO
执行流程:
线程 发请求1
线程 发请求2
线程 发请求3
线程 释放
然后:操作系统处理网络通信
当响应回来:线程池线程继续执行
所以:并发 ≠ 多线程,这里发生的是:IO并发,而不是:线程并发
这是 async 最重要的认知之一。
练习3(经典错误)
代码:
public async Task<string> Get()
{
return await Task.Run(() =>
{
return httpClient.GetStringAsync(url).Result;
});
}
问题:这个代码在高并发服务器上会产生什么问题?
提示:假设1000请求
假设服务器:线程池最大线程 = 200
有:1000请求
如果写成正确 async:
线程发HTTP请求
↓
线程释放
↓
等待IO
↓
线程继续执行
线程可能只需要:20~50个 就能处理。
如果写成错误代码:
Task.Run + .Result
执行模型:
线程池线程
↓
发HTTP
↓
阻塞等待
结果:200线程全部阻塞
剩下请求:排队等待线程。吞吐量会急剧下降