Rust in Everything
基于 rayon-core 1.13.0

全局池与 Registry

主线位置:主线 1 和主线 3 共用的舞台。join 推任务进的是 Registry 里某个 worker 的 deque,外部注入进的是 Registry::injected_jobs,睡眠计数器是 Registry::sleep。

Registry 的字段

registry.rs:128-151:

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_jobscrossbeam 的 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):

字段类型用途
primedLockLatch线程进入主循环时 set;build_global / wait_until_primed 等它
stoppedLockLatch线程退出主循环时 set;注释说只给测试用
terminateOnceLatchterminate_count 降到 0 时 set;主循环就是在等它
stealerStealer<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(&registry);

        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(&registry),
                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

registry.rs:156-207:

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(第一次建池就失败了)时才 expect panic。
  • 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 常见坑 单列。

不支持线程的平台:回退成单线程

default_global_registry:

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 µs3、1、040 µs、17 µs、27 µs
drop 前 spawn 一个 sleep 200 ms 的任务(3 次)84 ns、0 ns、42 ns0、0、0203.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 的任务。

💬 关联讨论

发起讨论
加载中...