Cannot start a runtime from within a runtime
主线位置:任务主线开头
block_on「进入运行时」那一步。这个线程已经进入过一次,第二次被拒绝。
完整报错:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.
同一类问题还有两句:
Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
Cannot block the current thread from within a runtime. This happens because a function attempted to block the current thread while the thread is being used to drive asynchronous tasks.
flowchart TB
t[线程正在驱动异步任务] --> a{又想阻塞}
a -->|"block_on"| p1["Cannot start a runtime"]
a -->|"drop Runtime"| p2["Cannot drop a runtime"]
a -->|"blocking_recv 等"| p3["Cannot block the current thread"]
t --> ok{改成}
ok --> f1[".await"]
ok --> f2[spawn_blocking]
ok --> f3[block_in_place]
触发它的代码
最常见的来源是一个「同步包装」:某个库或者自己早先写的函数,在内部建一个 Runtime 再 block_on,后来被从 async 代码里调用了。
// A "sync" helper that someone wrote with its own runtime inside
fn fetch_config_sync() -> String {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async { "config".to_string() })
}
fn main() {
quiet_hook();
let rt = tokio::runtime::Runtime::new().unwrap();
case(&rt, "1. sync helper with its own Runtime, called from async", async {
let _ = fetch_config_sync();
});
case(&rt, "2. Handle::current().block_on inside async", async {
tokio::runtime::Handle::current().block_on(async {});
});
case(&rt, "3. build a Runtime inside async and drop it", async {
let inner = tokio::runtime::Runtime::new().unwrap();
drop(inner);
});
case(&rt, "4. mpsc::Receiver::blocking_recv inside async", async {
let (tx, mut rx) = tokio::sync::mpsc::channel::<u8>(1);
tx.send(1).await.unwrap();
let _ = rx.blocking_recv();
});
case(&rt, "5. Mutex::blocking_lock inside async", async {
let m = tokio::sync::Mutex::new(0);
let _g = m.blocking_lock();
});
}
quiet_hook 只打印 panic 消息;case 在 catch_unwind 里 rt.block_on(fut)。输出:
1. sync helper with its own Runtime, called from async
panic: Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.
2. Handle::current().block_on inside async
panic: Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.
3. build a Runtime inside async and drop it
panic: Cannot drop a runtime in a context where blocking is not allowed. This happens when a runtime is dropped from within an asynchronous context.
4. mpsc::Receiver::blocking_recv inside async
panic: Cannot block the current thread from within a runtime. This happens because a function attempted to block the current thread while the thread is being used to drive asynchronous tasks.
5. Mutex::blocking_lock inside async
panic: Cannot block the current thread from within a runtime. This happens because a function attempted to block the current thread while the thread is being used to drive asynchronous tasks.
几个细节:
- 情况 1 里,在 async 中新建
Runtime本身不报错,报错的是block_on。panic 展开时内层Runtime被 drop,那次 drop 发现线程已经在 panic,就不再报第二句。 - 情况 3 没有
block_on,只有 drop:Runtime的 drop 要等它的阻塞线程池全部退出,这是一次阻塞等待。 - 在
#[tokio::main]的函数里调用另一个标了#[tokio::main]的函数,就是情况 1:宏展开后那个函数体里有一次block_on。
源码里在哪
三句话来自两个检查,都看线程本地变量 CONTEXT 里的 runtime 标志(这个线程是否正在驱动异步任务):
| 报错 | 位置 | 谁会走到 |
|---|---|---|
Cannot start a runtime from within a runtime | enter_runtime | Runtime::block_on、Handle::block_on |
Cannot block the current thread from within a runtime | future::block_on 里 try_enter_blocking_region 返回 None | blocking_recv、blocking_send、blocking_lock、blocking_read 等 |
Cannot drop a runtime in a context where blocking is not allowed | shutdown::Receiver::wait 里同一个检查 | Runtime 的 drop、shutdown_timeout |
enter_runtime 为什么不允许嵌套(内层 block_on 会让外层所有任务停摆,可能死锁)、block_on 线程和 worker 线程上这个标志各是什么,见线程本地上下文。drop 时等阻塞线程的过程见阻塞线程池。
判断依据是线程本地标志,不是「当前是不是 worker 线程」。所以多线程运行时的 block_on 里(主线程,不是 worker)照样报错;反过来,spawn_blocking 的线程上标志没设,在那里 block_on 是允许的。
问: 既然 spawn_blocking 的线程上可以 Handle::current().block_on(..),那它在 current_thread 运行时上也一样好用吗?
答: 允许,但有条件。Handle::block_on 的文档写明:在 current_thread 运行时上,只有 Runtime::block_on 能驱动 IO 和定时器驱动,Handle::block_on 不能;依赖 IO 或定时器的 future,要有另一个线程正在同一个运行时上 Runtime::block_on 才能完成。在 #[tokio::main(flavor = "current_thread")] 里试了两种情况(M2 Max、rustc 1.99.0,各跑 3 次,结果一致):
main在.await一个spawn_blocking,闭包里handle.block_on(async { sleep(10ms).await }):12ms 左右完成。main线程在Runtime::block_on里等待,顺带驱动着定时器。main用std::thread::JoinHandle::join等一个普通线程,那个线程里做同样的handle.block_on:1 秒后看门狗线程报告还卡着。唯一能驱动定时器的线程被join占住了。
怎么修
下面四种改法放在一个 #[tokio::main] 里:
fn fetch_config_sync() -> String {
let rt = Runtime::new().unwrap();
rt.block_on(async { "config".to_string() })
}
async fn fetch_config() -> String {
"config".to_string()
}
#[tokio::main]
async fn main() {
// Fix 1: you are already async -- just await
println!("1. await: {}", fetch_config().await);
// Fix 2: run the sync helper on a blocking thread (its runtime lives and dies there)
let v = tokio::task::spawn_blocking(fetch_config_sync).await.unwrap();
println!("2. spawn_blocking(sync helper): {v}");
// Fix 3: block_in_place + the current Handle (multi_thread only)
let v = tokio::task::block_in_place(|| Handle::current().block_on(fetch_config()));
println!("3. block_in_place + Handle::block_on: {v}");
// Fix 4: a runtime created in async must not be dropped with a blocking wait
let inner = Runtime::new().unwrap();
let v = inner.spawn(fetch_config()).await.unwrap();
inner.shutdown_background();
println!("4. inner runtime + shutdown_background: {v}");
}
1. await: config
2. spawn_blocking(sync helper): config
3. block_in_place + Handle::block_on: config
4. inner runtime + shutdown_background: config
1. 直接 .await。 报错信息自己给的建议。调用方已经是 async,就把被调的函数也改成 async,或者把它内部的 future 暴露出来。库同时提供同步和异步两套 API 时,async 代码里用异步那套。
.await」这半句是 1.54.0 才加的2021 年 7 月,有人在 #[tokio::test] 里调了 tokio_test::block_on,拿到这句 panic,在 issue #3960 里说:他真正需要的是报错直接提示一下,不用 block_on,.await 就行。2026 年 9 月的 #8501(1.54.0)关闭了这个 issue,给 enter_runtime 里的消息补上 If you are in an async function, use .await on the future instead of blocking on it.。PR 说明的理由是:原来的消息解释了运行时不能阻塞它的执行线程,「但没有告诉调用方怎么解决」。1.53 及更早的版本,这句报错到 ...drive asynchronous tasks. 就结束了。
2. spawn_blocking。 同步函数改不了(第三方库、内部建了自己的运行时),就把整个调用挪到阻塞线程上。阻塞线程没有进入运行时,里面可以 block_on,也可以建、可以 drop 一个新 Runtime。代价是闭包要 Send + 'static。
3. block_in_place + Handle::block_on。 不想把数据 move 出去,或者需要借用局部变量时用。block_in_place 把 worker 的职责交给别的线程,再把当前线程的标志暂时清掉,所以闭包里能 block_on。只能在多线程运行时用,在 current_thread 上会报另一句 panic。
4. 运行时建在 async 外面。 程序需要第二个运行时(比如专门跑某类任务),在 main 开头、进入任何运行时之前建好,或者在一个专门的 std::thread 里建。实在要在 async 里结束一个运行时,用 shutdown_background,它不等阻塞线程,所以不触发检查。
shutdown_background 是同一个 PR 加的0.2.22 之前,Runtime drop 时等阻塞线程池用的是 enter(false),在异步上下文里失败,panic 的是 Cannot start a runtime from within a runtime,可当时并没有谁在启动运行时。#2646(0.2.22)的说明把它叫作「一个令人费解的 panic」。这个 PR 做了两件事:换成现在这句专门的 Cannot drop a runtime in a context where blocking is not allowed;再加一个完全不阻塞的关闭方法,「作为确实需要这样做时的出口」,就是 shutdown_background。它的实现是 shutdown_timeout(Duration::from_nanos(0)),同一个 PR 在 Receiver::wait 开头加了「超时为 0 就直接返回」,所以它走不到那个检查。
blocking_recv、blocking_lock 这类方法本来就是给同步代码用的:在 async 代码里用 recv().await、lock().await;在同步代码里(spawn_blocking 的闭包、普通线程)用 blocking_*。
| 情况 | 改法 |
|---|---|
| 被调函数可以改 | 改成 async,.await |
| 同步函数改不了,参数能 move | spawn_blocking |
| 要借用局部变量,运行时是 multi_thread | block_in_place 里 handle.block_on |
| 需要第二个运行时 | 在进入运行时之前建;在 async 里结束用 shutdown_background |
blocking_recv / blocking_lock 报错 | async 里换成 .await 版本 |
小结
- 三句报错都是同一个检查:这个线程正在驱动异步任务,不能再阻塞等待。
block_on报第一句,blocking_*报第三句,dropRuntime报第二句。 - 检查的是线程本地标志:
block_on里和 worker 上设了,spawn_blocking线程上没设。 - 首选
.await;改不了的同步代码放进spawn_blocking;需要借用时在 multi_thread 上用block_in_place。 - 在 async 里新建运行时可以,
block_on它或者普通 drop 它不行,结束它用shutdown_background。