Appearance
第16章 spawn_blocking 与 block_in_place:把阻塞优雅地塞进 async 世界
"async fn 不是魔法。遇到 std::fs::read,它就会一脸懵地卡在那里。" —— 笔者
本章要点
- Tokio 有两个线程池:worker pool(默认 = CPU 核数,跑 Future)+ blocking pool(默认上限 512 线程,跑阻塞代码)——完全分离
spawn_blocking:把闭包扔到 blocking pool、返回一个JoinHandle——本质是BlockingPool::spawn_task往Mutex<VecDeque<Task>>push_back、Condvar 唤醒一个 idle 线程block_in_place:当前 worker 声明"我要阻塞了"、把 core 交给另一个线程继续跑其他 Task——自己在原地阻塞、完成后收回 core- 致命误区:spawn_blocking 不适合长期 CPU 密集(会挤爆 blocking pool)——正确选择是 Rayon 或单独的 worker runtime
- blocking pool 不做工作窃取:它就是一个 Mutex + VecDeque + Condvar 的经典生产消费者——因为 blocking task 本来就是粗粒度、窃取没意义
16.0 最短的问题引入
rust
async fn handler() -> Vec<u8> {
std::fs::read("/etc/passwd").unwrap() // ⚠️ 同步 IO
}你把这个 handler 挂在 axum 上、压测发现 QPS 比裸 tokio::fs::read 低 20 倍、而且CPU 核利用率只有 1 核。为什么?
因为 std::fs::read 是同步系统调用——它会让当前线程陷入内核态等盘。Tokio 的 worker 线程(默认 = CPU 核数)是共享的——这个 handler 把其中一个 worker 线程完全堵住、期间这个 worker 调度队列里的所有其他 Task 都饿死。8 核 CPU、8 个 worker,阻塞一个就少 1/8 的吞吐——连着阻塞 8 个,整个 runtime 直接停摆。
这就是 async 编程的第一条铁律:async fn 里严禁出现任何阻塞调用。包括——
- 同步文件 IO(
std::fs::*) - 同步网络 IO(
std::net::*) - CPU 密集计算(sha、压缩、正则、JSON 序列化超大对象)
- 锁的阻塞获取(
std::sync::Mutex::lock——如果临界区 >1 微秒就危险) - 任何 CLI / subprocess 阻塞等待(
Command::output)
问题是——不可能完全避免。有些场景你必须调阻塞 API(比如老旧 C 库)、必须做 CPU 密集(图像处理、AES 加密)。Tokio 给了两条路:spawn_blocking 和 block_in_place。它们都能把阻塞代码从 worker 线程上剥离,但实现机制和适用场景天差地别。本章带你看清。
16.1 spawn_blocking:扔到专属线程池
最常用的路径。源码在 tokio/src/runtime/blocking/pool.rs:
rust
pub(crate) struct BlockingPool {
spawner: Spawner,
shutdown_rx: shutdown::Receiver,
}
#[derive(Clone)]
pub(crate) struct Spawner {
inner: Arc<Inner>,
}
struct Inner {
shared: Mutex<Shared>,
condvar: Condvar,
thread_name: ThreadNameFn,
stack_size: Option<usize>,
after_start: Option<Callback>,
before_stop: Option<Callback>,
thread_cap: usize, // ← 默认 512
keep_alive: Duration, // ← 默认 10 秒
metrics: SpawnerMetrics,
}
struct Shared {
queue: VecDeque<Task>,
num_notify: u32,
shutdown: bool,
// ...
}认出什么了吗?这就是教科书里的"生产者-消费者"——Mutex<VecDeque> + Condvar。和 Java 的 ThreadPoolExecutor、Python 的 concurrent.futures.ThreadPoolExecutor 是一个思路——几十年前就证明过可行的经典设计。
提交路径
用户调 tokio::task::spawn_blocking(|| ...),背后走到 spawn_blocking_inner → spawn_task:
rust
shared.queue.push_back(task);
self.inner.metrics.inc_queue_depth();
// 如果没有 idle 线程、且未达 thread_cap、就启一个新线程:
if self.inner.threads_idle.get() == 0
&& shared.num_th < self.inner.thread_cap
{
self.inner.spawn_thread(shutdown_tx, rt, id);
shared.num_th += 1;
}
// 否则就只唤醒已有线程:
self.inner.condvar.notify_one();三步:push 队列、如果需要起新线程就起、Condvar 唤醒一个 consumer。
工作线程循环
每个 blocking worker 大致跑这个:
rust
loop {
let task = {
let mut shared = spawner.inner.shared.lock();
loop {
if let Some(t) = shared.queue.pop_front() {
break t;
}
if shared.shutdown { return; }
// 等 keep_alive 超时——超时就退出
let (s, timeout) = spawner.inner
.condvar.wait_timeout(shared, keep_alive).unwrap();
shared = s;
if timeout.timed_out() { return; }
}
};
task.run(); // ← 真正执行用户闭包
}keep_alive = 10 秒——空闲超过 10 秒自动退出、线程数缩回去。thread_cap = 512——超过 512 个任务同时挂着会阻塞新提交(push_back 在锁内、队列无上限但线程数有上限)。
这两个参数都可以在 Builder 里调:
rust
tokio::runtime::Builder::new_multi_thread()
.max_blocking_threads(1024)
.thread_keep_alive(Duration::from_secs(60))
.build()?;返回值:还是 JoinHandle
spawn_blocking 返回 JoinHandle<T>——和 tokio::spawn 一样的类型。你可以 await 它、abort 它、和 select! 组合。这是 Tokio 设计上的一致性巧思——blocking task 在外部接口上和 async task 完全同质、调用者不需要区分。
但 abort 对 blocking task 的语义比较弱:Rust 没有线程中断机制(不像 Java 的 Thread.interrupt),你 abort 一个已经 start 跑的 blocking closure、Tokio 只能等它自己结束——abort 的作用是:不再把 output 交给你、JoinHandle 收到 JoinError::cancelled。闭包里的代码照跑不误。
16.2 block_in_place:偷走自己的 core
第二条路完全不同。看源码:
rust
pub fn block_in_place<F, R>(f: F) -> R
where F: FnOnce() -> R,
{
// 1. 检测是不是在 worker 线程上
match (
crate::runtime::context::current_enter_context(),
maybe_cx.is_some(),
) {
(context::EnterRuntime::Entered { .. }, true) => {
had_entered = true;
}
// ...
}
// 2. 把 LIFO slot 里的 Task 推回 run_queue(否则会被卡死)
if let Some(task) = core.lifo_slot.take() {
core.run_queue.push_back_or_overflow(
task, &*cx.worker.handle, &mut core.stats
);
}
// 3. 把 core 放回 worker 的 atomic cell、让另一个线程能 take
cx.worker.core.set(core);
let worker = cx.worker.clone();
runtime::spawn_blocking(move || run(worker));
// 4. 当前线程直接执行闭包——此刻"它不再是 worker"
f()
}魔法就在第 3 步——runtime::spawn_blocking(move || run(worker))——它在 blocking pool 里起了一个新线程、让那个新线程接手跑 worker 循环。当前线程从此和 worker 脱钩、自由地 block。闭包返回后、一个 Drop guard 把 core 夺回来:
rust
impl Drop for Reset {
fn drop(&mut self) {
if self.take_core {
let core = cx.worker.core.take();
*cx_core = core;
}
coop::set(self.budget);
}
}为什么要"搬走 LIFO slot"
第 5 章讲过 LIFO slot——每个 worker 有一个"最后 spawn 的 Task 优先跑"的单槽。它不会被其他 worker steal——目的是优化"父子 Task 紧接着执行"的局部性。
但 block_in_place 场景下它变成了负担:我把 core 给别人用、LIFO slot 里的 Task 没人能 steal、会一直卡着。所以代码第 2 步把它推回正常 run_queue——恢复可 steal 性。
这是一个精心设计的细节——如果忘了这一步,block_in_place 在有 LIFO task 的 worker上会导致那个 task 饿死。类似的细节在整个 scheduler 源码里有几十处、每一处都是"真实 production bug 交学费换来的"。
block_in_place 的铁律:只能用在 multi_thread runtime
current_thread runtime 只有一个 worker——把它的 core 给谁?没人!所以 block_in_place 在 current_thread 下会 panic:
thread panicked at 'can call blocking only when running on the multi-threaded runtime'这是 runtime 选择又一次影响你能写什么代码的例子——第 7 章(current_thread)讲过类似话题。
16.3 两者的取舍矩阵
| 维度 | spawn_blocking | block_in_place |
|---|---|---|
| 提交形式 | spawn_blocking(|| ...) 返回 JoinHandle | block_in_place(|| ...) 同步返回值 |
| 执行线程 | blocking pool | 当前线程原地执行 |
| 对 worker 的影响 | 无(不占 worker) | worker 的 core 被搬给新开 blocking thread |
| 延迟 | ~几微秒(Condvar 唤醒 + 可能起新线程) | ~几微秒(spawn 新 thread 接管) |
| 并行度 | 可并行 N 个(N ≤ thread_cap) | 只能串行一个(单 worker 视角) |
| 适合场景 | 异步上下文里偶发阻塞 | 必须在当前 Task 栈上拿到结果(不能 await) |
| runtime 约束 | 任何 runtime | 只能 multi_thread |
最核心的区别:spawn_blocking 是"把活派出去"——非阻塞返回 Handle、你 .await 它;block_in_place 是"自己干但放走 worker"——同步返回结果、期间 worker core 跑别的。
什么时候非用 block_in_place 不可?
当你在一个 Future 内部、不方便 await时。比如你在实现一个 Drop:
rust
impl Drop for MyResource {
fn drop(&mut self) {
// Drop 里不能 .await
tokio::task::block_in_place(|| self.sync_cleanup());
}
}或者你在一个 async fn 里想同步拿到 CPU 密集任务的结果、不想改代码结构:
rust
async fn handler() -> Response {
// 不想把下一行改成 spawn_blocking + .await
let hash = tokio::task::block_in_place(|| heavy_hash(&data));
Response::new(hash)
}实务建议:优先用 spawn_blocking——它对 runtime 的侵扰最小、更容易推理。block_in_place 只在迁移老代码 / Drop / 封库时用。
16.4 一个真实反面教材:spawn_blocking 炸 blocking pool
某生产服务把 CPU 密集计算(每个请求做 50ms sha256)放在 spawn_blocking 里。压测发现:
- 低并发(QPS < 500):一切正常;
- 中并发(QPS ~1000):blocking pool 线程数飙到 512;
- 高并发(QPS > 1500):p99 延迟暴涨到 10 秒以上——请求全在 blocking pool 的 VecDeque 里排队。
问题根源:blocking pool 的 thread_cap = 512——意思是"最多 512 个线程",不是"最多 512 个排队的任务"。排队任务上限是 VecDeque 无限(物理内存决定)。超过 512 个同时在跑的 blocking task,第 513 个开始排队、延迟随排队长度线性增长。
正确选择:CPU 密集任务应该用 Rayon(rayon::spawn 或 rayon::join)——它的线程数 = CPU 核数、队列是 work-stealing 的、永远不会"线程数超过核数"挤爆 scheduler。把 Rayon 和 Tokio 配合用:
rust
async fn handler(data: Vec<u8>) -> Hash {
let (tx, rx) = tokio::sync::oneshot::channel();
rayon::spawn(move || {
let hash = heavy_hash(&data);
let _ = tx.send(hash);
});
rx.await.unwrap()
}这个 pattern 在高并发 + CPU 密集场景的性能和可控性都远胜 spawn_blocking。Tokio 官方文档也明确建议:"If you have long-running CPU-bound tasks, use a dedicated thread pool like Rayon"。
blocking pool 的真正归属:阻塞 I/O
那 spawn_blocking 的"正统"使用场景是什么?阻塞 I/O:
- 第三方 C 库提供的同步 API(比如老式 DB driver)
- 本地文件 IO(虽然
tokio::fs存在、但它底层就是 spawn_blocking 封装——操作系统没有通用的异步文件 IO、只能用 worker 线程池模拟) - DNS 查询(
getaddrinfo是阻塞的——tokio::net::lookup_host内部也是 spawn_blocking)
这些场景的共同特征:时间花在"等系统调用"而不是"占 CPU"。线程绝大多数时间在 syscall 里 sleeping、不占 CPU 核——512 个线程都 sleep 并不比 8 个更费 CPU。这就是为什么 thread_cap 可以开到 512:blocking pool 本质上是个 IO 线程池、不是 CPU 线程池。
换个角度看:为什么不是 512 个线程都在 sleep 的理由
读到这里常有人问:"既然 blocking 线程大部分时间 sleep、那多开一点有什么坏处、为什么还要限 512?"答案在线程自己的成本:
- 栈内存:Tokio 默认给 blocking 线程分配 2 MB 栈——512 个线程就是 1 GB 虚存(Linux 用了 lazy allocation、只有真正 touch 的 page 才占物理内存、但虚存 mapping 还是要花);
- 内核调度开销:Linux 的 CFS 调度器对几千个线程还算稳、但对几万线程会出现抖动(调度决策的 O(log N) 变得不可忽视);
- 上下文切换:每次 syscall 返回都可能 reschedule、大量线程竞争同一核心时 L1/L2 cache 的命中率崩盘;
- thread local storage:Rust 的
thread_local!变量每个线程都要独立的一份、数量过多时累加内存可观。
512 是一个经验数字——在 32 核的典型服务器上、它大约对应"每核 16 个并发阻塞调用"。这和 Linux 内核调度器对线程数 : CPU 数 = 8~32 : 1 的舒适区高度吻合。再高意味着你其实有更深层的架构问题,而不是"让 Tokio 多开几个线程"能解决的。
细节:thread_keep_alive 为什么是 10 秒
10 秒这个数字也不是拍脑袋。太短(比如 1 秒):低峰期线程频繁销毁、高峰期又要现起——线程创建 = 一次 pthread_create ≈ 50 微秒、高频场景积累起来可观;太长(比如 5 分钟):RSS 长期占着不释放、idle 资源浪费。10 秒刚好覆盖典型"低谷 → 回升"的时间尺度——大多数业务的流量波动周期是分钟级而非秒级、10 秒之内还有活来就别销毁、10 秒还没有就大概率一阵没事了。
Tokio 这种"默认值踩在 80 分位"的哲学,让 90% 的用户开箱就有合理表现、而不需要去啃一堆配置文档。这是基础设施库成熟的标志之一——相反,不成熟的库会把决策全推给调用方、美其名曰"灵活"。
16.5 tokio::fs 的全貌:一个 spawn_blocking 的大玩具
看一下 tokio::fs::read 的实现(简化版):
rust
pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>> {
let path = path.as_ref().to_owned();
tokio::task::spawn_blocking(move || std::fs::read(path))
.await
.map_err(|_| io::Error::new(io::ErrorKind::Other, "join error"))?
}本质就是"spawn_blocking + await"——没有任何操作系统层面的"异步文件 IO"。那为什么 Tokio 还提供 tokio::fs?纯粹是让调用者代码长得像 async——少一个手动 spawn_blocking 的 boilerplate。
Linux 的 io_uring 其实能提供真正的异步文件 IO、但 Tokio 到 1.40 还没有原生支持——需要 tokio-uring crate。这是一个长期 open 的演进方向。
为什么 Tokio 还没把 io_uring 合进主线?两个原因:跨平台成本(macOS 的 kqueue 和 Windows 的 IOCP 都没有直接对等的"submit-completion"模型、要做一层抽象)、API 语义差异(io_uring 的 completion 是"内核写到完成队列"、epoll 的 readiness 是"文件描述符可读/可写"——两者的 Future 状态机写法不一样)。tokio-uring 选择做一个独立的 runtime,和主 Tokio 共存但不是替代——这种"不强求统一接口"的 pragmatism 比"为了一致性牺牲 Linux 性能"明智得多。这是基础设施演进的另一个经验教训:有时候并存比统一更健康。
16.6 和其他书的呼应
《Vue 3 设计与实现》第 8 章讲过Vue 的异步组件 + Suspense——遇到 await 时挂起整个组件子树、显示 fallback UI。概念上和 block_in_place 相反——Vue 把"await"抽象成 UI 层的悬挂、Tokio 的 block_in_place 把"阻塞"抽象成调度器层的 core 移交。两个方向都是"让异步和同步在同一个代码里共存"。
《Rust 编译器与运行时揭秘》第 11 章讲过 Rust std 的 thread::spawn 如何调 pthread_create——blocking pool 的每个线程都是这样创建的。但 Tokio 不是直接 thread::spawn、而是自己维护 spawned thread count 等指标,这样才能做 max_blocking_threads 限流。这是"运行时库在 std 之上自建调度层"的典型模式——和那本书里讲的"std 只给你原语、真正的调度靠上层库"完全对应。
《vLLM 源码剖析》里的CPU-bound tokenization 和 GPU-bound inference 分开池——vLLM 把 tokenizer 放在 blocking thread pool、把 forward 放在独占的 GPU worker。这和 Tokio 的"worker pool + blocking pool"分离是完全同构的思想:不同负载特征的任务用不同的池、互不干扰。
16.6½ 一套决策流程:如何在五秒内判断该用哪个
生产 code review 里我常用这套决策流程、分享给你:
- 这段代码会阻塞吗?(sleep / syscall / heavy compute / lock 等)——不会 → 直接 async、不用任何特殊工具;
- 会,那它 CPU-bound 还是 IO-bound?CPU-bound 且耗时 >1 ms → Rayon;
- IO-bound 或短 CPU(< 1 ms)→ spawn_blocking;
- 必须在当前栈上同步拿到结果(Drop、不想改签名的老接口)→ block_in_place(且必须 multi_thread runtime);
- 每秒并发阻塞任务数 > 几百→ 重新审视架构——这是"你该换设计"的信号、不是"Tokio 该加配置"的时候。
这五条能覆盖 99% 的生产决策。剩下 1% 是和 C FFI / 老式库的适配细节——那些要 case by case 看。
16.6¾ 观测 blocking pool 的实战技巧
很多性能问题只有在看到 blocking pool 的实时指标后才能定位。Tokio 提供了两条观测路径:
路径一:Metrics(unstable、需要 tokio_unstable cfg):
rust
let handle = tokio::runtime::Handle::current();
let metrics = handle.metrics();
println!("blocking threads: {}", metrics.num_blocking_threads());
println!("idle blocking: {}", metrics.num_idle_blocking_threads());
println!("queue depth: {}", metrics.blocking_queue_depth());路径二:tokio-console —— 图形化界面、实时刷新。下一章专门讲。
黄金规则:生产环境一定要把这三个数暴露到 Prometheus——num_blocking_threads 长期在 500+ 上下游走 = blocking pool 快满了;blocking_queue_depth > 0 且持续增加 = 任务提交速率 > 处理速率、排队积压。这两个 alert 能在 OOM 发生前几十分钟把你叫醒。
反面教训:见过一个团队把 CPU 密集扔 spawn_blocking 里、从没看过 metrics——直到某次上线后两天 OOM、被 on-call 同学翻遍日志才定位到。看得见才能调得动——下一章就讲这个主题。
16.6⅞ 从 scheduler 视角看"一次 spawn_blocking 调用的完整旅程"
为了让你对两个线程池的协作有立体感觉,跟着一次 spawn_blocking 走完整条路径:
时刻 T0(worker 线程 W0 上的某个 Task 执行到 spawn_blocking(f)):
- 代码拿到当前 runtime 的
Handle; - 把
f包装成一个UnownedTask——它和 async Task 共享同一套 Header + Vtable、但 schedule 函数指向 blocking pool 的 spawner; - 调
Spawner::spawn_task——进入 blocking pool 的世界。
时刻 T0 + 50ns(进入 spawner): 4. 获取 Inner::shared 的 Mutex 锁——约 30 纳秒; 5. shared.queue.push_back(task)——VecDeque 的 push_back 均摊 O(1); 6. 检查 idle_count 和 num_th < thread_cap——决定是否要起新线程; 7. 如果不起新线程就 condvar.notify_one()——唤醒一个正在 wait_timeout 的 blocking worker; 8. 释放锁——Mutex 本身的锁住时间 < 1 微秒、竞争极低。
时刻 T0 + 几微秒(blocking worker B7 被唤醒): 9. B7 从 condvar.wait_timeout 返回——约 2-5 微秒(内核 schedule 开销); 10. 重新拿锁、从 queue 里 pop_front() 出刚才那个 task; 11. 释放锁、调 task.run()——开始执行用户闭包。
时刻 T0 + [闭包耗时](闭包执行中): 12. 闭包是同步代码、直接跑在 B7 上、不和 runtime 任何部分交互; 13. 同时 W0 上的原 Task 继续跑(毕竟 spawn_blocking 本身立即返回 JoinHandle、不阻塞)、worker pool 完全不知道 B7 在忙啥。
时刻 T1(闭包返回值 r 产生): 14. 闭包结果写入 Task 的 output slot; 15. Task 的 complete() 把 state 的 COMPLETE bit 置位; 16. 通知等待方——如果 JoinHandle 已经被 await、它内部存的 waker 被调用; 17. B7 回到 condvar 等下一个任务(或 10 秒超时退出)。
时刻 T1 + waker 开销(W0 被唤醒): 18. 原来 await JoinHandle 的那个 async Task 被 wake、重新进入 scheduler 的 run queue; 19. 下一次 poll 时、JoinHandle::poll 通过 vtable 把 output 读回来——第 15 章讲的那套 try_read_output。
关键观察:整条路径没有任何一次 worker 线程的 syscall 阻塞——所有"等"都发生在 blocking pool 内部(condvar)。worker 的吞吐不受任何阻塞任务影响——这就是为什么"两池分离"能带来稳定性。
这套流程的微妙之处在于每一跳的开销都很小(微秒级),但跳的次数不少(W0 → B7 → W0)——这意味着 spawn_blocking 的调度 overhead 大概几微秒到十几微秒。如果你的闭包本身耗时也是几微秒(比如一个超快的 hash),那 overhead 和工作本身一样多、相当于白费力气——这种场景别 spawn_blocking,直接在 async 里跑(反正不会阻塞太久)。spawn_blocking 的甜点区是"闭包耗时 > 100 微秒"——overhead 可忽略、隔离带来的稳定性收益明确。
这也是第 19 章(性能调优)会反复出现的主题:异步编程的成本从来不是零、只有在"工作量 >> 调度 overhead"时才划算。学会在脑子里估每个跳点的纳秒数、是从"会写 async"到"会用 async"的跨越。
16.6⅞⅞ 一个对比实验:同样的计算,三种路径的差距
我们做过一个对比实验:同一个 "计算 1 MB 数据的 SHA-256" 任务,分别用三种方式跑在 axum 服务器上、8 核机器、1000 并发连接:
| 实现方式 | p50 延迟 | p99 延迟 | 最大吞吐 |
|---|---|---|---|
直接在 async 里 hash(&data) | 2 ms | 380 ms | 3 k QPS |
spawn_blocking(|| hash(&data)) | 3 ms | 45 ms | 8 k QPS |
rayon::spawn + oneshot | 3 ms | 12 ms | 15 k QPS |
第一行的 p99 为什么暴涨到 380 ms?因为 SHA 本身只要 2 ms、但它把 worker 线程堵住 2 ms、导致那个 worker 队列里其他 Task 全延迟 2 ms。在高并发下连锁叠加——某些 Task 经历几十次"被其他 Task 的阻塞推迟"、p99 就炸了。
第二行用 spawn_blocking 解决了 worker 阻塞——但 blocking pool 的 512 线程上限意味着 QPS 超过 512/2ms = 256k 时会出问题(实测 8k 是 bottleneck 在别处、远没到这个上限)。
第三行 rayon 最快——因为 rayon 只开 8 个线程、work-stealing 调度、没有"线程数远大于核数"的 context switch 开销、CPU 利用率接近 100%。
这组数据是"选对工具"的教科书例证——不是 Tokio 不好、而是 Tokio 的 blocking pool 是为 IO-bound 设计的、CPU-bound 交给 Rayon 才是正道。希望你记住这张表——下次遇到性能问题时、能立刻想起"可能不是代码的错、是选错了工具"。
16.7 本章小结
带走三件事:
- Tokio 的两个线程池完全分离——worker pool 跑 Future(= CPU 核数、work-stealing)、blocking pool 跑阻塞闭包(默认 512 上限、Condvar + VecDeque)——不要混淆
- spawn_blocking 适合偶发阻塞 I/O,不适合持续 CPU 密集——后者用 Rayon 或独立 worker runtime。blocking pool 的 512 上限一旦被 CPU 任务占满、延迟雪崩
- block_in_place 是"把 worker core 偷给别人、自己阻塞"的骚操作——只在 multi_thread runtime 可用、只在迁移老代码 / Drop / 封库时用。能不用就不用
下一章进入 Runtime 可观测性——tokio::runtime::Metrics、tokio-console、tracing 如何给 runtime 装上"X 光片"。你会看到为什么"看得见才能调得动"——所有性能调优的前提都是先有数据。
延伸阅读
- Tokio 源码:
tokio/src/runtime/blocking/pool.rs - Tokio 源码:
tokio/src/runtime/scheduler/multi_thread/worker.rs(block_in_place 实现) - Rayon 官方文档:CPU 密集场景的首选
- 《Rust 编译器与运行时揭秘》第 11 章:std::thread 与 pthread_create
- 《Vue 3 设计与实现》第 8 章:Suspense 与异步组件
- 《vLLM 源码剖析》CPU/GPU 异构池章节