子线程 panic 了,crossbeam::scope 没有 panic
主线位置:不在三条调用链上。实现和完整的实验 E12 在 thread::scope 与 std 的 scope。
flowchart TB
child["未 join 的子线程 panic"] --> which{哪个 scope}
which -->|crossbeam::scope| err["返回 Err(Vec),载荷保留"]
which -->|std::thread::scope| pan["scope 自己 panic:a scoped thread panicked"]
joined["已显式 join 的子线程 panic"] --> jr["两边都由 join() 的 Err 报告,scope 不再报"]
现象
两种方向都有人踩:
- 用 crossbeam 时,以为子线程 panic 会传播出来,写了
crossbeam::scope(|s| { ... });而没处理返回值。子线程 panic 了,程序继续往下跑。返回值是thread::Result<R>,不用的话只有一个unused_must_use警告。 - 从 crossbeam 迁到
std::thread::scope时,原来写成match crossbeam::scope(..) { Err(panics) => 记日志并降级, .. }的代码没有对应物:std 的scope直接 panic,而且 panic 里没有子线程的原始载荷。
实测
实验 E12(exp-crossbeam/scope-panic/,rustc 1.99.1,release,3 次输出相同;std 一侧外面包了 catch_unwind 来观察):
crossbeam no-panic: Ok("scope body done")
std no-panic: returned "scope body done"
crossbeam child-panics: Err, is Vec<Box<dyn Any + Send>>=true, payloads=["child boom"]
std child-panics: panicked with "a scoped thread panicked"
[child-panics-joined] explicit join -> is_err=true
crossbeam child-panics-joined: Ok("scope body done")
[child-panics-joined] explicit join -> is_err=true
std child-panics-joined: returned "scope body done"
- 子线程 panic、没有显式 join:crossbeam 返回
Err,里面是所有 panic 载荷的Vec(这里是"child boom");std 的scope自己 panic,消息是固定的「a scoped thread panicked」,"child boom"只出现在默认 panic hook 打印的日志里。 - 子线程 panic,但作用域里已经对它的句柄
join()过:两边都把 panic 交给join()的返回值,作用域本身不再报告。
从哪来
crossbeam 的 scope 用 catch_unwind 跑闭包,等所有子线程结束后逐个 join 还没被 join 的句柄,把 Err 收集起来,非空就返回 Err(Box::new(panics))(thread::scope 与 std 的 scope)。std 的 scope 不收集句柄,只有一个 a_thread_panicked 标志,最后 panic!("a scoped thread panicked")。
crossbeam 的文档自 2023-01 起写明它 soft-deprecated、推荐 std 的版本(thread.rs:138),但没有 #[deprecated],编译不会提示,所以两种写法会在同一个代码库里长期共存。
怎么改
还在用 crossbeam:别丢掉返回值。要么 .unwrap()(子线程 panic 时整体 panic,和 std 的行为接近),要么显式处理 Err(panics)。
迁到 std,按原来怎么处理返回值分三种:
| 原来的写法 | std 里的写法 |
|---|---|
crossbeam::scope(..).unwrap() | 直接 std::thread::scope(..):子线程 panic 时同样 panic,只是消息变成「a scoped thread panicked」 |
match / if let Err(panics) 处理子线程 panic | 对每个要处理的子线程保留句柄,在作用域里 h.join(),自己处理 Err;join 过的线程不会再让 scope panic |
| 需要原始 panic 载荷(日志、上报) | 同上,从 join() 的 Err 里取;或在子线程闭包里自己 catch_unwind |
还有一处签名差异:crossbeam 的 spawn 闭包多一个 &Scope 参数(s.spawn(|_| ...)),std 是 s.spawn(|| ...),嵌套 spawn 直接借用外层的 s。
小结
- crossbeam:未 join 的子线程 panic 收集成
Err(Vec)返回,载荷保留;不处理返回值就等于吞掉。 - std:
scope自己 panic「a scoped thread panicked」,原载荷不在 panic 里。 - 显式 join 过的子线程,两边都由
join()的返回值报告。 - 迁移时,处理子线程 panic 的代码要改成逐个
join()。
💬 关联讨论
发起讨论加载中...