Rust in Everything
基于 rayon-core 1.13.0

Rayon: detected unexpected panic; aborting

主线位置:主线之外,detached 任务的 panic 去向。机制在 spawn 与 spawn_fifo,这里只给现象、复现和改法。

flowchart TB
  sp["rayon::spawn(f)"] --> run["worker 执行 HeapJob"]
  run --> cu["Registry::catch_unwind(f)"]
  cu -->|"f 正常返回"| ok["terminate_count 减一"]
  cu -->|"f panic"| guard["放一个 AbortIfPanic 守卫"]
  guard --> h{"有 panic_handler?"}
  h -->|"有,且 handler 正常返回"| keep["forget 守卫,进程继续"]
  h -->|"没有,或 handler 也 panic"| ab["守卫 drop:打印一行,process::abort()"]

现象

进程突然退出,stderr 最后一行是:

Rayon: detected unexpected panic; aborting

shell 里的退出码是 134(128 + SIGABRT)。panic 的消息本身照常打印在这行之前,但 main 里后面的代码一行都没跑;把调用 rayon::spawn 的地方包进 std::panic::catch_unwind 也没用。

复现

实验 E11(exp-rayon/panics/),主线程 spawn 一个会 panic 的闭包后睡 300 ms 再打印一行(全部结果见 spawn 与 spawn_fifo):

模式输出退出码
rayon::spawn,没有 handlerRayon: detected unexpected panic; aborting,主线程那行没打出来134
spawn_broadcast,0 号线程 panic,没有 handler同上134
spawn,build_global 时设了 panic_handlerhandler got detached,主线程照常打印0
spawn_broadcast,两个线程 panic,设了 handlerhandler 被调用 2 次,进程继续0

join、scope、broadcast、install 不会这样:它们阻塞调用方,panic 被接住后在调用方的线程上 resume_unwind,和普通函数调用里的 panic 一样可以被 catch_unwind 接住(scope 家族)。

从哪来

detached 任务执行时包了一层 Registry::catch_unwind:接住 panic 后先放一个 AbortIfPanic 守卫,有 handler 就调用它并 forget 守卫;没有 handler,函数结束时守卫被 drop。守卫的 Drop 就是那行 eprintln! 加 std::process::abort()(unwind.rs:24-31)。

这是有意的:spawn 没有返回句柄,panic 没有调用方可以传回,ThreadPoolBuilder::panic_handler 的文档(lib.rs:557-575)写明默认行为是 abort,理由是 panic 不应悄无声息地丢掉。start_handler / exit_handler 也走同一个 catch_unwind,它们 panic 一样会 abort。

catch_unwind 在外面拦不住,是因为 panic 发生在另一个线程(worker)上,调用 spawn 的线程早已返回。

怎么改

  • 给池设 panic_handler:全局池用 ThreadPoolBuilder::new().panic_handler(|err| ...).build_global(),要在任何 rayon 调用之前(否则 build_global 返回错误,见 The global thread pool has already been initialized);私有池在 build() 前设。handler 里记日志、计数,别再 panic。
  • 在闭包里自己接住:rayon::spawn(move || { let r = std::panic::catch_unwind(AssertUnwindSafe(job)); let _ = tx.send(r); }),把结果连同 panic 一起经 channel 送回去。配 tokio 时就是 spawn 与 spawn_fifo 里的 oneshot 写法,接收方拿到 Err 自己决定怎么办。
  • 能等就别 detached:需要结果、又要借用栈上数据的,用 scope / join,panic 会传回调用方。
  • 依赖的第三方库里有 rayon::spawn 时,它的 panic 也会走到你进程的全局池上;全局池的 handler 只能由最先初始化全局池的那段代码设置。

小结

  • 没有 panic_handler 的池里,spawn / spawn_fifo / spawn_broadcast 的闭包 panic 会 abort 整个进程,退出码 134(E11)。
  • 来源是 Registry::catch_unwind 里的 AbortIfPanic 守卫,默认行为是文档写明的设计。
  • 改法:设 handler、在闭包里 catch_unwind 并把结果送回、或者改用会传播 panic 的 scope / join。

💬 关联讨论

发起讨论
加载中...