Rust in Everything
基于 rayon-core 1.13.0

install 等待时,本池的其他任务插进来执行

主线位置:主线 1 的入口,「在别的池的 worker 上」那条路。机制和完整实验在 in_worker:从哪进池。

sequenceDiagram
  participant G as 全局池唯一 worker
  participant P as pool
  G->>G: join:推 B(另一个 do_it),跑 A 的 do_it
  G->>G: 打印 one
  G->>P: install:in_worker_cross 注入空闭包
  G->>G: wait_until:本地 deque 弹出 B,执行 do_it
  G->>G: 打印 one(同一线程、同一栈)
  G->>P: B 的 install,再 wait_until
  P-->>G: 两个 install 都完成
  G->>G: 打印 two、two

现象

全局池只有 1 个线程,另有一个私有池 pool。ThreadPool::install 文档里的例子:

rayon::ThreadPoolBuilder::new().num_threads(1).build_global().unwrap();
let pool = rayon::ThreadPoolBuilder::default().build().unwrap();
let do_it = || {
  print!("one ");
  pool.install(|| {});
  print!("two ");
};
rayon::join(|| do_it(), || do_it());

全局池只有一个线程,直觉上两次 do_it 应该顺序执行,打印 one two one two。实际打印的是 one one two two:第二个 do_it 在第一个还没打印 two 时就开始了,而且就在同一个线程上。

真实程序里的表现:一段代码在 install 前后假设「这期间本线程不会跑别的东西」(读写 thread_local、持有 RefCell 借用、持锁、按顺序打日志),在跨池调用时这些假设被打破。

复现

实验 E6(exp-rayon/cross-pool/),上面的例子重复 200 次:

cross_install=true: {"one one two two": 200}
cross_install=false: {"one two one two": 200}

在这个配置下不是「可能出现」,而是必然:唯一的全局 worker 跑 A 的 do_it,打印 one,install 把空闭包注入 pool 后在 wait_until 里找活,本地 deque 里正好有 B(另一个 do_it),于是在同一个栈上执行它。反向验证:把 pool.install(|| {}) 去掉,其他不变,200 次全是 one two one two。

从哪来

Registry::in_worker 发现当前线程是别的池的 worker,走 in_worker_cross:用 SpinLatch::cross 把任务注入目标池,然后当前线程 wait_until 这个 latch。wait_until 是 worker 的等待方式:边等边从自己的池里找活执行(WorkerThread 主循环)。这样设计是为了不让跨池调用把当前池的线程白白占住,也避免两个池互相等待时死锁(36577d6「Solve inter-pool deadlocks」,#449)。

文档把它写在 install 的「Warning: execution order」一节(thread_pool/mod.rs:78-110):当前线程会「try to keep busy while the op completes in its target pool, similar to calling ThreadPool::yield_now() in a loop」。

从不是 worker 的线程调 install(比如 main)没有这个现象:走的是 in_worker_cold,调用线程在 LockLatch 上纯阻塞,什么也不执行。

怎么改

  • 不依赖「install 期间本线程不跑别的」:要保护的状态别放在 thread_local 或跨 install 的 RefCell 借用里;锁不要跨 install 持有(同 持着 Mutex 调 par_iter,偶尔卡死)。
  • 需要顺序就显式排序:输出带上序号,最后排序;或者把要按顺序做的部分放在 install 之外、由一个线程串行完成。
  • 少做跨池调用:一个计算尽量在一个池里完成。从非 worker 线程(main、普通线程、tokio 的 spawn_blocking 线程)调 install,等待方式是阻塞,不会插入别的任务;代价是那个线程被占住。
  • 想让一个私有池里的计算完全隔离,就别在别的池的任务里调用它。

小结

  • 在 A 池的 worker 上调 B 池的 install,当前线程 wait_until 期间会执行 A 池的任务,文档例子在 1 线程全局池上 200/200 次是 one one two two(E6)。
  • 来源是 in_worker_cross + wait_until:worker 的等待从来不是阻塞。
  • 从非 worker 线程调 install 是纯阻塞,没有这个现象;依赖执行顺序或线程身份的代码要避开跨池等待。

💬 关联讨论

发起讨论
加载中...