Rust in Everything
基于 rayon-core 1.13.0

scope 里两个任务都 panic,只看到一个

主线位置:主线之外,阻塞型 API 的 panic 传播。机制在 scope 家族 和 join 与 join_context。

flowchart TB
  j0["job0 panic"] --> jp["job_panicked:槽位空?"]
  j2["job2 panic"] --> jp
  jp -->|"CAS 抢到"| slot["存进 ScopeBase.panic"]
  jp -->|"已被占或 CAS 失败"| drop["drop 掉这个 payload"]
  slot --> wait["所有任务结束"]
  wait --> prop["maybe_propagate_panic:resume_unwinding 槽里那一个"]

现象

rayon::scope 里 spawn 了几个任务,其中两个 panic。scope 返回时确实 panic 了(或者外层 catch_unwind 拿到了 Err),但 payload 里只有其中一个任务的消息;同一个程序跑几次,拿到的还不一定是同一个。另一个 panic 的消息只出现在 stderr 上(默认 panic hook 在每次 panic 时都会打印),程序里拿不到。

复现

实验 E11(exp-rayon/panics/),每组 20 次(scope 家族):

模式结果
4 个任务,job0、job2 panic,四组传播 job0 / job2 的次数分别为 12/8、6/14、8/12、12/8;每组 80 个任务全部执行
op spawn 一个会 panic 的任务后自己也 panic,四组传播 op / job 分别为 14/6、18/2、18/2、20/0
join,A、B 都 panic传播 A 的
join,只有 B panic(A 睡 50 ms)传播 B 的,且 A 已执行完

后两行见 join 与 join_context。

从哪来

ScopeBase 只有一个 panic: AtomicPtr<Box<dyn Any + Send>> 槽位。job_panicked 先看槽位是否已被占,没有才 CAS 抢;抢不到的那个 payload 当场 drop,注释是「capture the first error we see, free the rest」。所有任务结束后 maybe_propagate_panic 把槽里的那一个 resume_unwinding。谁先存进去取决于谁先执行完,所以不确定;op 在当前线程上立刻 panic,而任务要等别人来偷,所以 op 多数时候赢。

join 没有槽位,规则是固定的:先等 B 结束(B 可能借用栈上数据),然后 A panic 就传 A 的,A 正常而 B panic 才传 B 的(join_recover_from_panic)。

scope 的文档保证「已 spawn 的任务都会执行」:一个任务 panic 不会取消其他任务,它们照常跑完。

怎么改

  • 要所有错误:别用 panic 表达任务失败。任务返回 Result,经 channel 或预先分好的槽位(Vec<Mutex<Option<E>>>、每个任务写自己的下标)送回,scope 结束后统一检查。
  • 每个任务各自接住:在任务体里 catch_unwind,把 payload 转成错误放进上面的收集结构;这样一个任务的 panic 也不会被另一个「顶掉」。
  • 想让第一个错误尽快停掉其他任务:用并行迭代器的 try_for_each / try_reduce,它们有共享的停止标志(fold、reduce 与 sum);scope 本身没有取消机制。

小结

  • scope 只传播一个 panic:第一个存进 AtomicPtr 的,其余 drop,哪一个不确定(E11)。
  • join 规则固定:两边都 panic 传 A 的,只有 B panic 时等 A 跑完再传 B 的。
  • 要收齐错误,就让任务返回 Result 或各自 catch_unwind,自己收集。

💬 关联讨论

发起讨论
加载中...