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,但它卡住主线程。