Rust in Everything
基于 tauri 2.12.5

Cannot start a runtime from within a runtime

主线位置:调用主线的「async 命令 respond_async_serialized 丢进 async_runtime」一步。命令体是 tokio 工作线程上的一个任务,block_on 在那里是被禁止的。

flowchart TB
  asyncc["async 命令"] --> worker["tokio-rt-worker 上的任务"]
  worker --> bo["async_runtime::block_on"]
  bo --> pan["tokio panic"]
  pan --> lost["任务结束 Promise 永远不落定"]
  syncc["同步命令"] --> mainth["主线程"]
  mainth --> ok["block_on 可以 但卡住事件循环"]
  mainth --> tm["tokio::main 下 panic 进程 abort"]

实验 crate pit2-app:从模板 base 复制,tauri =2.12.5,--features tauri/custom-protocol,隐藏窗口,debug 构建,MODE=blockon。会让进程 abort 的那一种没有重新跑,用的是异步运行时那一页的实验 app-life 留下的输出。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

一个 async 命令要用一段 async 代码的结果,写成了 block_on:

async fn fetch_value() -> u32 {
  tokio::time::sleep(Duration::from_millis(10)).await;
  42
}

#[tauri::command]
async fn blockon_async() -> u32 {
  log("blockon_async: async_runtime::block_on(fetch_value())");
  tauri::async_runtime::block_on(fetch_value())
}

页面调用时套一个 1 秒的超时:

      out.blockonAsync = await attempt(() => within(inv('blockon_async'), 1000));
  554ms [tokio-rt-worker] blockon_async: async_runtime::block_on(fetch_value())

thread 'tokio-rt-worker' (2744519) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.54.1/src/runtime/scheduler/multi_thread/mod.rs:89:9:
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.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
...
 "blockonAsync": {
  "err": "no answer after 1000 ms"
 },

终端里有一行 panic,前端什么都收不到:没有结果,也没有错误。不加超时的 await invoke('blockon_async') 会一直挂着。应用本身没死,后面的调用照常进行(见下面修法的完整输出)。

用 #[tokio::main] 写 main 时,换成同步命令里 block_on 也会撞上这句话,而且后果更重。app-life 设 RT=tokiomain 时模仿 #[tokio::main]:先建一个运行时交给 async_runtime::set,再在它的 block_on 里启动 tauri:

  if rt_mode == "tokiomain" {
    // what #[tokio::main] does: the main thread sits inside Runtime::block_on
    let rt = tokio::runtime::Builder::new_multi_thread()
      .thread_name("rie-rt")
      .enable_all()
      .build()
      .unwrap();
    tauri::async_runtime::set(rt.handle().clone());
    rt.block_on(async { start() });
  }

同步命令 block_sync 里调 tauri::async_runtime::block_on:

  564ms [main] block_sync: async_runtime::block_on on this thread

thread 'main' (2470140) panicked at src/main.rs:87:3:
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.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
exit=134

panic 发生在主线程的同步命令里,进程 abort,退出码 134。为什么是 abort 而不是普通的 panic 退出,是本部分「failed to initiate panic, error 5, aborting」那一坑的内容:展开要穿过 wry 的 start_task 和 WebKit 的栈帧,找不到接住它的地方。

出处

tauri::async_runtime::block_on 就是 tokio 的 Runtime::block_on / Handle::block_on:

pub fn block_on<F: Future>(task: F) -> F::Output {
  let runtime = RUNTIME.get_or_init(default_runtime);
  runtime.block_on(task)
}

tokio 在进入 block_on 时检查当前线程是否已经处在某个运行时的上下文里,是就 panic。这个检查和这句消息的来历见 tokio 卷 Cannot start a runtime from within a runtime。在 tauri 里会落进「已经在运行时里」的有两种线程:

线程什么代码在上面block_on
tokio-rt-worker(或 set 进来的运行时的工作线程)async 命令、async_runtime::spawn 的任务、tauri:// 协议的处理panic,任务结束
main,且 main 在 Runtime::block_on 里(#[tokio::main])同步命令、setup、插件钩子、run 的回调panic,同步命令里会 abort
main,普通的 fn main同上能用,但期间事件循环停着

async 命令的 Promise 为什么永远不落定:async 命令的包装代码把命令体交给 respond_async_serialized,后者在 respond_async_serialized_inner 里 async_runtime::spawn 一个任务,task.await 之后才调 return_result 应答。panic 发生在 task.await 里,tokio 在 poll 任务时接住它、结束任务,return_result 那一步就没了。spawn 返回的 JoinHandle 被直接丢掉,没有人等它、把 JoinError 转成一个错误应答。

修法

在 async 命令里,block_on 想做的事就是 .await:

#[tauri::command]
async fn blockon_await() -> u32 {
  log("blockon_await: fetch_value().await");
  fetch_value().await
}

调用链深处有一个同步函数、改不成 async,必须在里面同步等一个 future 时,先用 tokio::task::block_in_place 告诉 tokio「这个工作线程要阻塞了」,再在里面 block_on:

#[tauri::command]
async fn blockon_in_place() -> u32 {
  log("blockon_in_place: block_in_place + block_on");
  tokio::task::block_in_place(|| tauri::async_runtime::block_on(fetch_value()))
}

同步命令在普通 fn main 的主线程上,block_on 本身合法:

#[tauri::command]
fn blockon_sync() -> u32 {
  log("blockon_sync: async_runtime::block_on on the main thread");
  tauri::async_runtime::block_on(fetch_value())
}

MODE=blockon 依次调用 blockon_async、blockon_await、blockon_in_place、blockon_sync:

    0ms [main] main
   94ms [main] probe plugin setup
  554ms [tokio-rt-worker] blockon_async: async_runtime::block_on(fetch_value())

thread 'tokio-rt-worker' (2744519) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tokio-1.54.1/src/runtime/scheduler/multi_thread/mod.rs:89:9:
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.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
 1555ms [tokio-rt-worker] blockon_await: fetch_value().await
 1568ms [tokio-rt-worker] blockon_in_place: block_in_place + block_on
 1581ms [main] blockon_sync: async_runtime::block_on on the main thread
REPORT {
 "mode": "blockon",
 "blockonAsync": {
  "err": "no answer after 1000 ms"
 },
 "blockonAwait": {
  "ok": 42
 },
 "blockonInPlace": {
  "ok": 42
 },
 "blockonSync": {
  "ok": 42
 }
}
 1592ms [main] ExitRequested { code: Some(0) }
 1593ms [main] Exit
exit=0
  • .await 和 block_in_place 都拿到了 42。block_in_place 只在多线程运行时上可用,tauri 默认的运行时就是多线程的;用 async_runtime::set 换成 current-thread 运行时时它会 panic,见 tokio 卷 can call blocking only when running on the multi-threaded runtime。
  • blockon_sync 也拿到了 42,但这 10 毫秒里主线程停着:所有 invoke、资源加载、窗口操作都在排队,等的时间长了就是参数、返回值与 InvokeResolver 里「同步命令卡住的是什么」那一节的问题。如果被 block_on 的 future 里又要读窗口属性,就成了死锁(等主线程的人正是主线程)。所以同步命令里的 block_on 只适合很短、不碰窗口的 future;能改成 async 命令就改。
  • 用 #[tokio::main] 的应用:同步命令、setup、插件钩子里都不能 block_on。最省事的是不用 #[tokio::main],普通 fn main 里需要自己的运行时就建一个、交给 async_runtime::set,像异步运行时里那样。

怎么确认没中招

  • 终端里出现 Cannot start a runtime from within a runtime,后面跟着线程名:tokio-rt-worker 是 async 命令或任务里的 block_on;main 加上 fatal runtime error 是 #[tokio::main] 下同步代码里的 block_on。
  • 前端某个 invoke 永远不返回、也不报错,先看终端有没有 tokio-rt-worker 的 panic。
  • 搜 block_on(,包括 tauri::async_runtime::block_on 和 tokio::runtime::Handle::current().block_on,确认它们只出现在普通 fn main 的主线程同步代码里,或者包在 block_in_place 里。

小结

  • async 命令在 tokio 工作线程上执行,block_on 会 panic;tokio 接住 panic、结束任务,JS 的 Promise 永远不落定。
  • #[tokio::main] 让主线程处在运行时上下文里,同步命令里的 block_on 同样 panic,而且因为在主线程的同步命令里,进程 abort(134)。
  • 修法:async 里用 .await;非要同步等,用 block_in_place 包住;普通 fn main 的同步命令里可以 block_on,但它卡住主线程。

💬 关联讨论

发起讨论
加载中...