全局池与 Registry
主线位置:主线 1 和主线 3 共用的舞台。
join推任务进的是Registry里某个 worker 的 deque,外部注入进的是Registry::injected_jobs,睡眠计数器是Registry::sleep。
Registry 的字段
pub(super) struct Registry {
thread_infos: Vec<ThreadInfo>,
sleep: Sleep,
injected_jobs: Injector<JobRef>,
broadcasts: Mutex<Vec<Worker<JobRef>>>,
panic_handler: Option<Box<PanicHandler>>,
start_handler: Option<Box<StartHandler>>,
exit_handler: Option<Box<ExitHandler>>,
// When this latch reaches 0, it means that all work on this
// registry must be complete. This is ensured in the following ways:
// ...
terminate_count: AtomicUsize,
}
| 字段 | 是什么 | 谁读写 |
|---|---|---|
thread_infos | 每个 worker 一个 ThreadInfo,下标就是线程编号 | 窃取者读 stealer;terminate 写 terminate latch |
sleep | 睡眠计数器 + 每线程的 CachePadded<WorkerSleepState> | 05 sleep 模块 |
injected_jobs | crossbeam 的 Injector<JobRef>:池外提交的任务 | inject 写,pop_injected_job 读(Injector) |
broadcasts | 每个 worker 一条 FIFO 广播队列的 Worker 端,锁着 | inject_broadcast 持锁 push(broadcast) |
| 三个 handler | 从 builder 拿过来 | catch_unwind、main_loop |
terminate_count | 引用计数,到 0 时让所有线程退出 | 见下文 |
ThreadInfo(registry.rs:613-631):
| 字段 | 类型 | 用途 |
|---|---|---|
primed | LockLatch | 线程进入主循环时 set;build_global / wait_until_primed 等它 |
stopped | LockLatch | 线程退出主循环时 set;注释说只给测试用 |
terminate | OnceLatch | terminate_count 降到 0 时 set;主循环就是在等它 |
stealer | Stealer<JobRef> | 这个线程本地 deque 的窃取端 |
注意两个容易混的 stealer:ThreadInfo::stealer 是本地 deque 的,给别的线程偷;WorkerThread::stealer 是本线程广播队列的,给自己取广播任务(WorkerThread 主循环)。
Registry 永远放在 Arc 里,它的地址就是池的身份:RegistryId { addr } 就是 self as *const Self as usize,in_worker 用它判断「当前线程是不是本池的 worker」。
Registry::new:每线程两条 deque
registry.rs:239-322。先算线程数(截到 max_num_threads(),见 ThreadPoolBuilder),然后为每个线程建两条 crossbeam deque:
let (workers, stealers): (Vec<_>, Vec<_>) = (0..n_threads)
.map(|_| {
let worker = if breadth_first {
Worker::new_fifo()
} else {
Worker::new_lifo()
};
let stealer = worker.stealer();
(worker, stealer)
})
.unzip();
let (broadcasts, broadcast_stealers): (Vec<_>, Vec<_>) = (0..n_threads)
.map(|_| {
let worker = Worker::new_fifo();
let stealer = worker.stealer();
(worker, stealer)
})
.unzip();
- 本地 deque:默认 LIFO,废弃的
breadth_first下 FIFO(两种 flavor 的区别见 Chase-Lev 双端队列)。Worker端交给将来的 worker 线程,Stealer端放进thread_infos。 - 广播 deque:一律 FIFO。方向反过来:
Worker端留在Registry::broadcasts里(广播方 push),Stealer端交给 worker 线程(自己取)。
这一对设计,加上每个 worker 还有一个 JobFifo(内部是一个 Injector,给 spawn_fifo 用,JobRef 与几种 Job),就是 rayon-core 用到的全部 crossbeam 队列:每线程两条 deque + 一个 Injector,全池一个 Injector。
接着建 Arc<Registry>,terminate_count 初值 1,然后逐个起线程:
// If we return early or panic, make sure to terminate existing threads.
let t1000 = Terminator(®istry);
for (index, (worker, stealer)) in workers.into_iter().zip(broadcast_stealers).enumerate() {
let thread = ThreadBuilder {
name: builder.get_thread_name(index),
stack_size: builder.get_stack_size(),
registry: Arc::clone(®istry),
worker,
stealer,
index,
};
if index == 0 && builder.use_current_thread {
// ...(见 ThreadPoolBuilder 一页)
continue;
}
if let Err(e) = builder.get_spawn_handler().spawn(thread) {
return Err(ThreadPoolBuildError::new(ErrorKind::IOError(e)));
}
}
// Returning normally now, without termination.
mem::forget(t1000);
Terminator 是一个 drop 守卫,析构时调 registry.terminate()(registry.rs:230-236)。第 k 个线程起不来时提前 return Err,守卫把 terminate_count 从 1 减到 0,给每个线程的 terminate latch 置位,已经起来的 0..k 号线程看到后退出。全部成功就 mem::forget 守卫。每个 ThreadBuilder 都持有一个 Arc<Registry>,所以 registry 活得比所有 worker 都久。
全局池:static mut + Once
static mut THE_REGISTRY: Option<Arc<Registry>> = None;
static THE_REGISTRY_SET: Once = Once::new();
/// Starts the worker threads (if that has not already happened). If
/// initialization has not already occurred, use the default
/// configuration.
pub(super) fn global_registry() -> &'static Arc<Registry> {
set_global_registry(default_global_registry)
.or_else(|err| {
// SAFETY: we only create a shared reference to `THE_REGISTRY` after the `call_once`
// that initializes it, and there will be no more mutable accesses at all.
debug_assert!(THE_REGISTRY_SET.is_completed());
let the_registry = unsafe { &*ptr::addr_of!(THE_REGISTRY) };
the_registry.as_ref().ok_or(err)
})
.expect("The global thread pool has not been initialized.")
}
fn set_global_registry<F>(registry: F) -> Result<&'static Arc<Registry>, ThreadPoolBuildError>
where
F: FnOnce() -> Result<Arc<Registry>, ThreadPoolBuildError>,
{
let mut result = Err(ThreadPoolBuildError::new(
ErrorKind::GlobalPoolAlreadyInitialized,
));
THE_REGISTRY_SET.call_once(|| {
result = registry().map(|registry: Arc<Registry>| {
// SAFETY: this is the only mutable access to `THE_REGISTRY`, thanks to `Once`, and
// `global_registry()` only takes a shared reference **after** this `call_once`.
unsafe {
ptr::addr_of_mut!(THE_REGISTRY).write(Some(registry));
(*ptr::addr_of!(THE_REGISTRY)).as_ref().unwrap_unchecked()
}
})
});
result
}
flowchart LR
call["第一次 rayon 调用或 build_global"] --> once{"THE_REGISTRY_SET.call_once"}
once -->|"第一次:执行闭包"| new["Registry::new"]
new -->|Ok| store["写入 THE_REGISTRY,返回 Ok"]
new -->|Err| fail["返回 Err;global_registry 随后 panic"]
once -->|"已执行过"| already["返回预设的 Err(GlobalPoolAlreadyInitialized)"]
already --> g{"调用者"}
g -->|"global_registry()"| read["读 THE_REGISTRY,返回已有的池"]
g -->|"build_global"| err["把 Err 交给用户"]
读法:
result先设成Err(GlobalPoolAlreadyInitialized)。只有第一个进call_once的调用会执行闭包、把result改成建池的结果;之后所有调用call_once什么也不做,拿回的就是那个预设的Err。build_global直接返回这个Result(经init_global_registry),所以第二次build_global报GlobalPoolAlreadyInitialized。global_registry()用默认配置去试一次;拿到Err不要紧,or_else去读THE_REGISTRY:已经有了就返回它。只有当THE_REGISTRY仍是None(第一次建池就失败了)时才expectpanic。static mut只在call_once内写一次,读都在call_once之后,Once提供了写和读之间的 happens-before。代码用ptr::addr_of!/addr_of_mut!取地址,不产生对static mut的引用。
谁会调 global_registry():任何不在 worker 上的 rayon 调用,in_worker 的冷路径、Registry::current()、current_num_threads()、spawn、scope 等。所以第一次 rayon 调用就是全局池的初始化,之后的 build_global 只能失败。全局池的 terminate_count 初值 1 从不释放,它的线程活到进程结束。
实测:只能初始化一次(E10)
exp-rayon/global-init 的 after_use:先 rayon::join(|| 1, || 2),再 ThreadPoolBuilder::new().num_threads(4).build_global()。3 次运行输出完全相同:
join -> (1, 2), threads=12
build_global after join -> Err(ThreadPoolBuildError { kind: GlobalPoolAlreadyInitialized })
Display: The global thread pool has already been initialized.
threads still=12
反向验证(first):build_global 作为第一次调用,3 次都返回 Ok(()),池大小是设定的 3;紧接着第二次 build_global(num_threads(5)) 返回同样的 Err,池大小仍是 3。
first build_global -> Ok(()), threads=3
second build_global -> Err(ThreadPoolBuildError { kind: GlobalPoolAlreadyInitialized }), threads=3
Display 的文字来自 lib.rs 里的常量 GLOBAL_POOL_ALREADY_INITIALIZED(lib.rs:750-751);上游的 rayon-core/tests/double_init_fail.rs 断言的也是这句。实际后果是「谁先调用谁决定配置」:库代码或测试框架里先跑过一次 par_iter,main 里的 build_global 就悄悄失效,若用 .unwrap() 则直接 panic。这条坑在 09 常见坑 单列。
不支持线程的平台:回退成单线程
fn default_global_registry() -> Result<Arc<Registry>, ThreadPoolBuildError> {
let result = Registry::new(ThreadPoolBuilder::new());
// If we're running in an environment that doesn't support threads at all, we can fall back to
// using the current thread alone. This is crude, and probably won't work for non-blocking
// calls like `spawn` or `broadcast_spawn`, but a lot of stuff does work fine.
//
// Notably, this allows current WebAssembly targets to work even though their threading support
// is stubbed out, and we won't have to change anything if they do add real threading.
let unsupported = matches!(&result, Err(e) if e.is_unsupported());
if unsupported && WorkerThread::current().is_null() {
let builder = ThreadPoolBuilder::new().num_threads(1).use_current_thread();
let fallback_result = Registry::new(builder);
if fallback_result.is_ok() {
return fallback_result;
}
}
result
}
起线程返回 io::ErrorKind::Unsupported(wasm32-unknown-unknown、wasm32-wasi 上的 std 就是这样)时,用 num_threads(1).use_current_thread() 重建:调用线程自己成为唯一的 worker,但不跑主循环。于是 join 退化成顺序执行;spawn 推进去的任务没人主动去取,除非调用线程之后通过 broadcast、yield_now 等让出(crate 文档「Global fallback when threading is unsupported」一节,lib.rs:22-37)。只对隐式创建的全局池生效,显式的 build / build_global 照常返回错误。这个回退在 rayon-core 1.11.0(rayon 1.7.0)加入(9e62075,#1019)。
回退带来了一个后续 bug:全局池建好之后,调用线程本身变成了 worker,而 in_place_scope 在建池之前就判断过「当前线程不是 worker」。rayon-core 1.13.0 的修法是建池后重新检查(scope/mod.rs:414-426,注释「A new global registry may use the current thread, especially on WebAssembly, so we have to re-check our current status after it's built」),RELEASES.md 1.11.0 一节记为「Fixed a bug in in_place_scope when the default global registry uses the current thread」。
本卷没有在 wasm 上做实验,以上只按代码和文档。
当前池
Registry::current() 和 current_num_threads() 的规则一样:当前线程是某个池的 worker,就是那个池;否则是全局池(必要时现场初始化)。rayon::spawn、rayon::broadcast、rayon::current_num_threads 都按这条规则找池。「当前池」完全由线程本地的 WORKER_THREAD_STATE 决定,in_worker:从哪进池 讲它的后果。
terminate_count 与 ThreadPool::drop
terminate_count 的注释(registry.rs:137-150)列出了谁持有引用:全局池持有一个永不释放的;用户的 ThreadPool 持有一个,drop 时释放;install / join / scope 都是阻塞的,执行期间调用方持有 ThreadPool,不需要额外计数;只有 spawn 例外,它的任务可能比 ThreadPool 活得久,所以每个 spawn 出去的任务各持一个计数,执行完再释放(spawn 与 spawn_fifo)。
pub(super) fn increment_terminate_count(&self) {
let previous = self.terminate_count.fetch_add(1, Ordering::AcqRel);
debug_assert!(previous != 0, "registry ref count incremented from zero");
assert!(previous != usize::MAX, "overflow in registry ref count");
}
pub(super) fn terminate(&self) {
if self.terminate_count.fetch_sub(1, Ordering::AcqRel) == 1 {
for (i, thread_info) in self.thread_infos.iter().enumerate() {
unsafe { OnceLatch::set_and_tickle_one(&thread_info.terminate, self, i) };
}
}
}
(registry.rs:585-600)减到 0 的那一次,给每个线程的 terminate latch 置位,并叫醒正睡着的那个线程(set_and_tickle_one)。worker 的主循环一直在 wait_until(terminate),看到 set 就退出。按计数的语义,这时不应再有未完成的任务,退出路径上只有一句 debug_assert!(self.take_local_job().is_none()) 检查本地队列为空(WorkerThread 主循环)。
ThreadPool::drop 只做一件事:
impl Drop for ThreadPool {
fn drop(&mut self) {
self.registry.terminate();
}
}
它不 join 线程,也不等已 spawn 的任务。
实测:drop 不等线程
exp-rayon/global-init 的 drop_no_wait:4 线程池,exit_handler 计数;先 broadcast 一次确保线程都起来了,然后 drop,记录 drop 本身的耗时、drop 刚返回时有几个线程已退出、4 个线程全部退出的时刻。
| 场景 | drop 耗时 | drop 返回时已退出 | 4 个全部退出于 |
|---|---|---|---|
| 无未完成任务(3 次) | 34.9 µs、5.3 µs、3.2 µs | 3、1、0 | 40 µs、17 µs、27 µs |
drop 前 spawn 一个 sleep 200 ms 的任务(3 次) | 84 ns、0 ns、42 ns | 0、0、0 | 203.3 ms、205.0 ms、205.1 ms |
- 无任务时,drop 返回那一刻退出了几个线程是随机的(3 / 1 / 0):drop 只置位 latch、叫醒线程,不等它们。
- drop 前 spawn 一个任务时,
terminate_count是 2,drop 只减到 1,几十纳秒就返回;线程们在任务完成、计数归零之后才退出,即约 200 ms 之后。这也是ThreadPool文档那句「they will complete executing any remaining work that you have spawned, and automatically terminate」的实现。
需要「确认线程都退出了」的场合,只能自己用 exit_handler 计数,或者用 build_scoped(std::thread::scope 会 join)。
小结
ThreadPool只是Arc<Registry>。Registry里:每线程的ThreadInfo(primed/stopped/terminate三个 latch 和本地 deque 的Stealer)、sleep、全局Injector、锁着的广播队列、handler、terminate_count。- 每个线程两条 deque:本地(LIFO)和广播(FIFO),方向相反;
Terminator守卫保证建池中途失败时已起的线程会退出。 - 全局池是
static mut+Once:第一次 rayon 调用就用默认配置初始化,此后build_global一律Err(GlobalPoolAlreadyInitialized)(E10 实测);第一次建池就失败时global_registry()panic。 - 不支持线程的平台上,隐式的全局池回退成「调用线程当唯一 worker」。
terminate_count:ThreadPool持 1,每个spawn的任务各持 1;ThreadPool::drop只减计数,不等线程,也不等 spawn 的任务。