外观
第 4 节:async 里的阻塞——一个 thread::sleep,卡住整个服务器
症状
async 程序(第 16 章/第 20 章小网站)里混进了 std::thread::sleep、大文件读取、CPU 密集循环……整个程序莫名卡住——其他请求全部排队,好像服务器死了。
诊断
async 的"分身"是假的:一个运行时线程(worker)上跑着很多任务,它们靠 .await 轮流干活。await 是"我等着,把位置让给别人";thread::sleep 是"我占着位置睡,谁也别想用"——它把整个 worker 线程卡死,这个线程上排队的任务全部陪葬。
实测(任务里 thread::sleep(2s),主任务只想等 0.1 秒):
实测数据
text
阻塞版:
主任务想 0.1 秒后继续……(但被阻塞卡住) ← 2 秒后才打印!
总耗时 2.001 秒
正确版(tokio::time::sleep):
主任务 0.1 秒就继续了! ← 准时继续
总耗时 2.017 秒阻塞版里,主任务等 0.1 秒的请求,硬生生等了 2 秒——因为和阻塞任务挤在同一个 worker 上。多线程运行时里,被卡住的只是部分 worker,其他 worker 上的任务还能跑——但卡不卡、卡多久,取决于调度,完全不可预测。这就是阻塞最可怕的地方:不是每次都炸,是"看运气"。
解药
解药一:async 世界里用 async 版 API:
| 阻塞操作 | async 版 |
|---|---|
std::thread::sleep | tokio::time::sleep |
std::fs::read_to_string | tokio::fs::read_to_string |
std::net::TcpListener | tokio::net::TcpListener(第 20 章的进阶版) |
| 标准库 HTTP | reqwest(内部是 async) |
解药二:真躲不开的阻塞,丢给专门线程(spawn_blocking):
rust
use tokio::task;
// 大文件读取、CPU 密集计算、老库调用……放这里
let result = task::spawn_blocking(|| {
std::fs::read_to_string("很大的文件.txt").unwrap()
}).await.unwrap();spawn_blocking 把活丢到"阻塞专用线程池"——async 主战场不受影响。
解药三:CPU 密集计算也小心——for i in 0..10_000_000_000 这种循环不 await,一样卡 worker。大计算同样 spawn_blocking,或者拆成小块轮流 await。
预防
- 在 async 函数里看到"标准库同步 IO/睡眠",先警惕:
std::thread::sleep、std::fs::、std::net::都是嫌疑人(第 20 章小网站的handle_connection是同步的——它被spawn_blocking级的地方用才安全) reqwest/tokio::fs/tokio::io是 async 版标配:先想"有没有 async 版",再用- 服务器"卡死"排查顺序:①有没有阻塞操作混进 async?②锁竞争?(性能篇第 3 节)③死锁?(本卷第 1 节)
spawn_blocking的池也有上限:海量阻塞任务一样排队——阻塞是"能避免就避免"