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,自己收集。