一句话总结
异步 Rust 的陷阱大多源于一个根因:在不该阻塞的地方阻塞了运行时线程.
本篇汇总五个高频坑--阻塞调用,CPU 密集占用,持锁过 await,忘记 await,误以为相邻 await 并发--并给出每个的识别方法与正确写法.
一个根因串起大多数坑
回顾第 02,05 篇:异步运行时用少数几个 worker 线程承载海量任务,靠的是"任务一等待就让出线程".这个模型有一条铁律:
绝不要在异步任务里阻塞 worker 线程
一个 worker 线程可能正背着成百上千个任务.在某个任务里执行了阻塞调用或长时间纯计算,这个线程无法去 poll 其他任务,它身上所有任务集体卡死.
在 Go 里 runtime 会把阻塞的 goroutine 挪走,补充线程;Tokio 默认不会--这是 Go 程序员转 Rust 最致命的认知差.
陷阱一:在异步里调用阻塞 API
最常见,危害最大.在 async 里调用同步阻塞函数(std::thread::sleep,同步文件/网络库,阻塞的数据库驱动等):
|
正确做法分两类:
- 有异步替代品就用它:
tokio::time::sleep().await,tokio::fs::read().await. - 没有异步版(如某个只提供同步接口的 CPU/阻塞库),用
spawn_blocking把它挪到专门的阻塞线程池:
|
Tokio 的异步 worker 线程像餐厅前厅的服务员,要灵活穿梭服务多桌;spawn_blocking 的线程池像后厨,专门处理耗时的体力活.
把阻塞任务丢进后厨,前厅才不会因为一个慢活而瘫痪.
陷阱二:CPU 密集计算霸占 worker
即便没有"阻塞调用",一段纯计算如果耗时很长(比如几百毫秒的图像处理,加密),同样会霸占 worker 线程不让出--因为计算中间没有 .await 让出点.
|
解法同样是 spawn_blocking(或专门的 rayon 线程池):把 CPU 密集活儿移出异步 worker.这也呼应第 01 篇的结论--异步不擅长 CPU 密集,该交给线程.
陷阱三:持锁跨越 await
第 07,08 篇详谈过,这里作为陷阱再强调.持有 std::sync::MutexGuard 跨 .await 会编译失败(非 Send);但即便用 tokio::sync::Mutex 绕过编译,长时间持锁过 await 仍会扼杀并发--所有等锁的任务全堵住.
|
陷阱四:忘记 .await
第 03 篇的"惰性"在实践中最常见的翻车现场:创建了 Future 却忘了 .await(也没 spawn),那段异步逻辑根本没执行.
|
靠编译器警告兜底
好在 Future 标了 #[must_use],这种情况编译器会给出 "unused implementer of Future" 警告.
务必把这类警告当错误对待,别让它淹没在输出里.这是 Go 没有的坑--Go 的 go f() 一写就跑,不存在"忘记触发".
陷阱五:误以为相邻 await 是并发
第 06,10 篇点过,这里归档为正式陷阱.两个相邻 .await 是顺序执行的,不会自动并发:
|
当两个操作互不依赖,本可并行时,写成相邻 await 就白白浪费了时间.想并发,用 join! 或 spawn.
自查清单
| 症状 | 可能的坑 | 修复 |
|---|---|---|
| 并发数上去后整体卡顿,延迟飙升 | 阻塞调用 / CPU 霸占 worker | 异步替代品或 spawn_blocking |
| 高并发下吞吐不升反降,任务堆积抢锁 | 持锁过 await | 缩短临界区,锁外做慢操作 |
| 某段异步逻辑"没生效",无报错 | 忘记 await | 补 .await,重视 must_use 警告 |
| 明明并发写法却和串行一样慢 | 相邻 await 未并发 | 改用 join! / spawn |
快速回顾
- 根因铁律:绝不在异步任务里阻塞 worker 线程;Tokio 不像 Go runtime 那样自动补救.
- 阻塞调用:用异步替代品,或
spawn_blocking把阻塞代码挪到专用线程池. - CPU 密集:无 await 让出点的长计算同样霸占 worker,交给
spawn_blocking/线程. - 持锁过 await:扼杀并发;缩短临界区,慢操作移出锁.
- 忘记 await:Future 惰性,漏 await 等于没执行;把 must_use 警告当错误.
- 相邻 await 非并发:互不依赖的操作用
join!/spawn才真正并行.
动手练习
- 阻塞对比:写一个在异步任务里调用
std::thread::sleep的程序,用多个并发任务观察它如何拖垮整体,再换tokio::time::sleep对比. - spawn_blocking:把一段耗时纯计算用
spawn_blocking移出 worker,验证其他任务恢复流畅. - must_use 警告:故意写一个忘记
.await的异步调用,确认编译器的 must_use 警告并修复. - join! 并发:把两个独立的异步请求从相邻 await 改成
join!,计时对比耗时. - 自查回顾:对照"自查清单",回顾练习中写过的代码,看是否踩过其中任何一条.