Rust in Everything
基于 crossbeam-utils 0.8.23

子线程 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()。

💬 关联讨论

发起讨论
加载中...