can call blocking only when running on the multi-threaded runtime
主线位置:主线之外。
block_in_place要把 worker 的Core交给另一个线程,current_thread 运行时没有这样的线程。
完整报错:
can call blocking only when running on the multi-threaded runtime
flowchart TB
b[block_in_place] --> w{在哪里调用}
w -->|multi_thread worker| ok1[交出 Core,原地阻塞]
w -->|"multi_thread 的 block_on"| ok2[直接执行]
w -->|运行时之外| ok3[直接执行]
w -->|current_thread| p[panic]
w -->|LocalSet 里| p
触发它的代码
最常见的场景是测试。代码在 #[tokio::main](默认多线程)下一直正常,写测试时 #[tokio::test] 默认是 current_thread:
use std::time::Duration;
fn load_index() -> usize {
std::thread::sleep(Duration::from_millis(20)); // pretend: blocking file read
42
}
async fn handler() -> usize {
tokio::task::block_in_place(load_index)
}
fn main() {
// LocalSet on a multi_thread runtime: also refused
let rt = tokio::runtime::Runtime::new().unwrap();
let local = tokio::task::LocalSet::new();
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
rt.block_on(local.run_until(handler()))
}));
println!("inside LocalSet on multi_thread: panicked = {}", r.is_err());
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn default_flavor() {
assert_eq!(handler().await, 42);
}
#[tokio::test(flavor = "multi_thread")]
async fn multi_thread_flavor() {
assert_eq!(handler().await, 42);
}
}
cargo test 的输出:
running 2 tests
tests::default_flavor --- FAILED
.
failures:
---- tests::default_flavor stdout ----
thread 'tests::default_flavor' (15649636) panicked at src/bin/bip.rs:9:3:
can call blocking only when running on the multi-threaded runtime
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::default_flavor
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
cargo run 的输出(多线程运行时,但在 LocalSet 里):
thread 'main' (15649458) panicked at src/bin/bip.rs:9:3:
can call blocking only when running on the multi-threaded runtime
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
inside LocalSet on multi_thread: panicked = true
所以「运行时是多线程的」还不够,调用点还不能在 LocalSet 里。#[tokio::main(flavor = "current_thread")] 下当然也一样。
问: 多线程运行时里别的 worker 都还在跑,在 LocalSet 里 block_in_place 阻塞一下,别的线程不是照样能接手吗?
答: 别的线程能接手的只有 worker 的 Core,也就是那个 worker 的本地队列和 LIFO 槽。LocalSet 里的任务不在任何 Core 里,它们排在 LocalSet 自己的队列上,而且可以是 !Send 的,只能在当前这个线程上 poll。block_in_place 交得出 Core,交不出 LocalSet:这个线程一阻塞,同一个 LocalSet 里的其他任务就全部停住,和在 current_thread 上阻塞是一回事。所以 LocalSet 在 poll 期间把 allow_block_in_place 临时清成 false(LocalSet::poll、RunUntil::poll),让它报和 current_thread 同一句 panic。
源码里在哪
报错文本在 maybe_move_runtime 里,它返回 Err,由 block_in_place 在第 404 行 panic!。判断依据是线程本地的 EnterRuntime::Entered { allow_block_in_place }:current_thread 的 block_on 进入运行时时传 false,LocalSet 被 poll 期间也临时设成 false。
LocalSet 到 1.46.0 才把 block_in_place 完全挡住0.2.19 之前,在 spawn_blocking 的闭包里调 block_in_place 会撞上一个断言失败(issue #2327)。#2410(0.2.19)重新划了界:运行时之外(不用 tokio、在 spawn_blocking 闭包里)可以阻塞,多线程运行时的 block_on 里也可以;「在 LocalSet 和 basic scheduler 的 block_on 里阻塞仍然不允许」。LocalSet 这一侧的实现,是在 RunUntil::poll 里清掉标志。
这留了个口子:直接 .await 一个 LocalSet 走的是 LocalSet::poll,LocalSet 被 drop 时析构任务走的是 Drop for LocalSet,两条路都不经过 RunUntil。2025 年的 issue #7368 报告「spawn_local 任务里的 block_in_place 不 panic」,#7372(1.46.0)给这两处也加上了同样的守卫。1.54.1 里一共三处:LocalSet::poll、Drop for LocalSet、RunUntil::poll。
完整的判断表、为什么 current_thread 必须拒绝(只有一个线程,阻塞就等于整个运行时停摆),以及多线程上 Core 怎么交接,阻塞线程池的「block_in_place:把 Core 交出去」一节已经讲过。allow_block_in_place 这个标志在哪里设、LocalSet 怎么改它,见线程本地上下文。
怎么修
测试改成多线程
只是测试里报错、生产代码跑在多线程运行时上,最小的改动是:
#[tokio::test(flavor = "multi_thread")]
async fn multi_thread_flavor() {
assert_eq!(handler().await, 42);
}
上面的输出里这个测试通过了。需要固定线程数可以加 worker_threads = 2。
改用 spawn_blocking
spawn_blocking 在任何运行时上都能用,包括 current_thread 和 LocalSet 里:
use std::time::Duration;
use tokio::runtime::{Handle, RuntimeFlavor};
fn load_index() -> usize {
std::thread::sleep(Duration::from_millis(20));
42
}
// block_in_place on multi_thread, spawn_blocking elsewhere (not LocalSet-aware)
async fn load_index_async() -> usize {
match Handle::current().runtime_flavor() {
RuntimeFlavor::MultiThread => tokio::task::block_in_place(load_index),
_ => tokio::task::spawn_blocking(load_index).await.unwrap(),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("spawn_blocking on current_thread: {}", tokio::task::spawn_blocking(load_index).await.unwrap());
println!("flavor-aware helper on current_thread: {}", load_index_async().await);
}
spawn_blocking on current_thread: 42
flavor-aware helper on current_thread: 42
代价是闭包要 Send + 'static,不能借用局部变量。借用是选 block_in_place 的主要理由,如果阻塞代码不需要借用,直接用 spawn_blocking 最简单。
按运行时类型选
上面的 load_index_async 用 Handle::current().runtime_flavor() 在多线程上走 block_in_place,其他情况走 spawn_blocking。它挡不住 LocalSet 的情况。在默认的 #[tokio::main] 里:
let local = tokio::task::LocalSet::new();
let flavor = local.run_until(async { Handle::current().runtime_flavor() }).await;
println!("runtime_flavor() inside LocalSet: {flavor:?}");
println!("{}", local.run_until(load_index_async()).await);
runtime_flavor() inside LocalSet: MultiThread
thread 'main' (15666345) panicked at src/bin/bip_local.rs:11:35:
can call blocking only when running on the multi-threaded runtime
LocalSet 里看到的运行时类型还是 MultiThread,于是走了 block_in_place,照样 panic。代码可能在 LocalSet 里被调用,就只用 spawn_blocking。
| 情况 | 改法 |
|---|---|
| 只有测试报错 | #[tokio::test(flavor = "multi_thread")] |
| 库代码,不知道调用方用什么运行时 | spawn_blocking |
| 必须借用局部变量,确定在多线程运行时的普通任务里 | block_in_place |
想两边兼顾,且不会在 LocalSet 里调用 | 按 runtime_flavor() 选择 |
小结
block_in_place只在多线程运行时的普通任务里(或多线程的block_on里、运行时之外)可用。current_thread 和LocalSet里会 panic。#[tokio::test]默认是 current_thread,这是最常见的触发场景。- 不需要借用时,
spawn_blocking在哪都能用,是更通用的选择。 - 按
runtime_flavor()分支可以兼顾两种运行时,但识别不出LocalSet。