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,没有 handler | Rayon: detected unexpected panic; aborting,主线程那行没打出来 | 134 |
spawn_broadcast,0 号线程 panic,没有 handler | 同上 | 134 |
spawn,build_global 时设了 panic_handler | handler got detached,主线程照常打印 | 0 |
spawn_broadcast,两个线程 panic,设了 handler | handler 被调用 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。