Rust in Everything
基于 tokio 1.54.1

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 runtimeenter_runtimeRuntime::block_on、Handle::block_on
Cannot block the current thread from within a runtimefuture::block_on 里 try_enter_blocking_region 返回 Noneblocking_recv、blocking_send、blocking_lock、blocking_read 等
Cannot drop a runtime in a context where blocking is not allowedshutdown::Receiver::wait 里同一个检查Runtime 的 drop、shutdown_timeout

enter_runtime 为什么不允许嵌套(内层 block_on 会让外层所有任务停摆,可能死锁)、block_on 线程和 worker 线程上这个标志各是什么,见线程本地上下文。drop 时等阻塞线程的过程见阻塞线程池。

判断依据是线程本地标志,不是「当前是不是 worker 线程」。所以多线程运行时的 block_on 里(主线程,不是 worker)照样报错;反过来,spawn_blocking 的线程上标志没设,在那里 block_on 是允许的。

📝NOTE
没有蠢问题

问: 既然 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 代码里用异步那套。

📝NOTE
冷知识:「use .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,它不等阻塞线程,所以不触发检查。

📝NOTE
冷知识:「Cannot drop a runtime」和 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
同步函数改不了,参数能 movespawn_blocking
要借用局部变量,运行时是 multi_threadblock_in_place 里 handle.block_on
需要第二个运行时在进入运行时之前建;在 async 里结束用 shutdown_background
blocking_recv / blocking_lock 报错async 里换成 .await 版本

小结

  • 三句报错都是同一个检查:这个线程正在驱动异步任务,不能再阻塞等待。block_on 报第一句,blocking_* 报第三句,drop Runtime 报第二句。
  • 检查的是线程本地标志:block_on 里和 worker 上设了,spawn_blocking 线程上没设。
  • 首选 .await;改不了的同步代码放进 spawn_blocking;需要借用时在 multi_thread 上用 block_in_place。
  • 在 async 里新建运行时可以,block_on 它或者普通 drop 它不行,结束它用 shutdown_background。

💬 关联讨论

发起讨论
加载中...