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是纯阻塞,没有这个现象;依赖执行顺序或线程身份的代码要避开跨池等待。