Rust in Everything
基于 rayon 1.12.0

在本站中

本站的 Cargo.toml 里没有 rayon,代码里也搜不到 rayon::、par_iter。Cargo.lock 里的 rayon 1.12.0 和 rayon-core 1.13.0 全是间接依赖,和本卷的版本完全一致。这一页回答四个问题:谁把它带进来、在哪个进程里真的会跑、本站的哪条代码路径会走到它、本卷 09 常见坑 哪些在本站碰得到。

flowchart TB
  site["本站 app(server 构建)"] --> search["module-search"]
  search --> tantivy["tantivy 0.27.0"]
  tantivy --> rayon["rayon 1.12.0"]
  rayon --> rcore["rayon-core 1.13.0"]
  rcore --> deque["crossbeam-deque 0.8.6"]
  tantivy --> meta["私有池 segment_updater:1 线程"]
  tantivy --> merge["私有池 merge_thread_i:4 线程"]
  desk["本站 app(desktop feature)"] --> dd["dioxus-desktop 0.7.9"]
  dd --> image["image 0.25.10(默认 feature 含 rayon)"]
  image --> rayon
  image --> ravif["ravif → rav1e → maybe-rayon、av-scenechange"]
  ravif --> rayon
  image --> exr["exr 1.74.0"]
  exr --> rcore

锁定的版本

本站仓库根目录的 Cargo.lock(写作时 main 的 3292b81):

crate本站 lock本卷差异
rayon1.12.01.12.0无
rayon-core1.13.01.13.0无
crossbeam-deque0.8.60.8.80.8.7 的 #1233 是 Worker::push 等的性能优化,对 rayon 没有行为影响,逐条判断见 crossbeam 卷 版本差异

网关 crates/gateway 有自己的 Cargo.lock,里面没有 rayon。

依赖链

在本站目录下用 cargo tree --offline --locked -i rayon -e normal 和 -i rayon-core 反查(只解析 lock,不编译),三种口径:

口径命令附加参数rayon 从哪来
lock 里的全部路径--all-features --target alltantivy 0.27.0 ← module-search;image 0.25.10、ravif 0.13.0、av-scenechange 0.14.1、maybe-rayon 0.1.1(后三个都在 rav1e 0.8.1 下)← dioxus-desktop;rayon-core 另有一条 exr 1.74.0 ← image,直接依赖 rayon-core 不经 rayon
生产服务器-p app -F server --target x86_64-unknown-linux-gnu只有 tantivy ← module-search 一条
wasm 前端-p app --target wasm32-unknown-unknown没有:rayon 不在依赖图里
桌面端-p app -F desktop --target aarch64-apple-darwinimage 一族,image 的默认 feature 打开了 rayon,ravif 开 threading,maybe-rayon 开 threads

所以生产进程里的 rayon 只有 tantivy 一个用户。module-search 的 tantivy 依赖是 optional 的,只在它的 server feature 下打开,app 的 server feature 把 module-search/server 带上。桌面端那一族只在打开 desktop feature 时编进来,生产构建没有;桌面端是否真会执行到 rayon 代码(图标解码之类大概不会进 avif 编码和 exr),本页没有核实,属推测。

tantivy 怎么用 rayon

tantivy 0.27.0 的 src/ 里 rayon 只出现在三个文件:core/executor.rs、error.rs(错误类型转换)、indexer/segment_updater.rs(git grep)。没有一处 par_iter、join,也没有调用全局池的函数;它只自己建 ThreadPool,用 spawn 和 scope。

搜索:Executor,本站走单线程

Executor 有两个变体:SingleThread 和 ThreadPool(Arc<rayon::ThreadPool>)。multi_thread 用 ThreadPoolBuilder 建一个池,线程名是前缀加编号;map 在多线程模式下 pool.scope,给每个段 spawn 一个任务,结果经 crossbeam 无界 channel 发回,scope 返回后按下标放回原位(本卷 scope 家族 末尾拿它做过例子)。

Index 的 executor 默认是单线程:唯一构造 Index 结构的 open_from_metas 写死 Executor::single_thread(),要多线程得显式调 set_multithread_executor / set_default_multithread_executor(线程名前缀 tantivy-search-)。本站全仓库搜不到这两个调用,module-search 的 engine 模块只 Index::open_or_create 加注册 jieba 分词器。所以本站的查询不经过 rayon:/api/search/query 这个 server 函数在 tokio worker 上直接调 searcher.search,各段在调用线程上顺序跑(tantivy 卷 Searcher;换成多线程 executor 时要在建 reader 之前设,见 executor 设晚了)。

写入:SegmentUpdater 的两个池

每个 IndexWriter 在 SegmentUpdater::create 里建两个 rayon 池:

池线程数线程名panic_handler跑什么
pool1segment_updater没有schedule_task:收新段、提交(清删除、写 meta.json、垃圾回收、consider_merge_options)、合并收尾
merge_thread_poolnum_merge_threads,默认 4(index_writer.rs:60-62)merge_thread_{i}有:只记一行 error!start_merge:合并几个段

两者都是 pool.spawn,即本卷 spawn 讲的 HeapJob + terminate_count。结果不经 rayon 传回:schedule_task 和 start_merge 各建一个 FutureResult,底下是 oneshot crate 的 channel,任务结束时 send。单线程的元数据池本身就是一把「串行化所有元数据修改」的锁:所有 schedule_task 的闭包在同一个线程上按 FIFO(池外 spawn 进注入队列)依次执行。

本站 module-search 每次写入(replace_all / upsert_documents / delete_documents)都新开一个 index.writer(50_000_000),用完 commit 后 wait_merging_threads 再丢掉。50 MB 的预算按每线程至少 15 MB(index_writer.rs:29-32)算,最多 3 个索引线程(index.rs:753-763,在至少 3 核的机器上),所以每次写入建起 3 个索引线程(std::thread,名字 thrd-tantivy-index{i})+ 1 个 segment_updater + 4 个 merge_thread_*,写完全部退出(tantivy 卷 IndexWriter 里数过线程)。也就是说本站每写一次就建、拆两个 rayon Registry;rayon 的全局池在生产进程里从未被初始化(tantivy 不调全局函数,别的依赖没有 rayon;由 git grep 和依赖树推出,没有在运行的进程里核对)。

写入时等在哪里

本站的增量重建(main 里 tokio::spawn 的每 10 分钟循环)和管理员的 search_reindex 都在 async 函数里直接调 engine 模块的同步写入函数,没有 spawn_blocking。调用线程是 tokio worker,它在三处阻塞,都不是 rayon 的 latch:

  1. commit() → PreparedCommit::commit → schedule_commit(..).wait():FutureResult::wait 是 oneshot::Receiver::recv(),等元数据池把提交做完。
  2. wait_merging_threads 先 join 索引线程(std::thread::JoinHandle)。
  3. 再调 wait_merging_thread:merge_operations.wait_until_empty()。MergeOperationInventory 是 census 0.4.2 的 Inventory,wait_until_empty 在一个 Mutex + Condvar 上等存活的 MergeOperation 计数归零。每个合并任务的闭包拥有一个 MergeOperation,任务结束时 drop,计数减一并 notify。

合并线程那一侧也有一处阻塞:合并写完后 end_merge 把收尾交给元数据池,再 .wait() 等它(第 700 行)。这是一个 rayon worker 阻塞在 oneshot 上、等另一个池的任务。它不会饿死:元数据池的任务(consider_merge_options → start_merge)只往合并池 spawn、把返回的 FutureResult 直接 drop,从不反过来等合并池(读源码得出)。

所以本卷讲的 in_worker_cold / LockLatch 在本站不出现:tantivy 从池外进 rayon 只用 spawn,不用 install、join、scope(多线程 executor 才用 scope,本站没开)。本站等 rayon 任务完成,靠的是 tantivy 自己的 oneshot 和 census 的 Condvar。

常见坑在本站

逐条对照 09 常见坑,依据是上面读过的 tantivy 0.27.0 源码和本站 module-search:

坑本站会不会碰到
spawn 里 panic,进程 abort合并池设了 panic_handler:合并本身的 panic 被 catch_unwind 接住、以 Err 发回,再以特定消息 resume_unwind 交给 handler,不会 abort。元数据池没设 handler:schedule_task 的闭包里若 panic(例如 RwLock 中毒时的 unwrap、SegmentManager 里的几处 expect),按 spawn 一章的规则会打印「Rayon: detected unexpected panic; aborting」并结束整个服务进程。正常运行时这些 unwrap 不会失败,所以是「能碰到但需要先有别的 panic」(推测,没有构造复现)
持锁调 rayon不会:tantivy 的池内任务不再调 join / scope / par_iter,没有「等待时执行偷来的任务」的机会
在 async 里直接调 par_iter不是这个机制,但后果同类:写入函数在 tokio worker 上阻塞于 oneshot::recv 和 census 的 Condvar,提交和合并期间占住一个 tokio worker。本站是 multi_thread 运行时,影响限于这个 worker 上排队的任务(spawn 一章 E5 的 multi_thread 结论)。改法同那一页:包 spawn_blocking
小任务并行反而慢不涉及:没有并行迭代器;每次写入建、拆两个池的线程创建开销,tantivy 卷 IndexWriter 测过很便宜
build_global 返回 GlobalPoolAlreadyInitialized不会:没人调 build_global,全局池从不初始化
RAYON_NUM_THREADS不起作用:两个池都显式 num_threads(1) / num_threads(4),环境变量只在 num_threads 为 0 时才读(ThreadPoolBuilder)
install 期间执行别的任务不会:没有 install
池里读不到调用线程的 thread_local没有发现依赖:合并和元数据任务需要的状态都 clone 进闭包(SegmentUpdater 是 Arc);没有逐个函数核对 tantivy 依赖的 TLS,属推测
scope 只传播一个 panic不会:scope 只在多线程 executor 里用,本站没开
par_sort 在小数组上更慢、filter 后 collect、par_bridge 乱序、浮点求和不确定不会:tantivy 不用 rayon 的并行迭代器、并行排序;BM25 打分是单线程算的,结果确定
池内阻塞等待有阻塞,没有饿死:合并线程在 end_merge 里阻塞等元数据池,跨池且被等的一方从不等回来;元数据池只有 1 个线程,它的任务里没有 .wait() 另一个 schedule_task(读源码得出)。如果以后 tantivy 在元数据任务里同步等元数据池自己的任务,1 线程的池会立刻卡死

小结

  • 本站没有直接依赖 rayon;lock 里是 rayon 1.12.0、rayon-core 1.13.0,与本卷相同;crossbeam-deque 是 0.8.6,比本卷旧两个补丁版本,没有行为差异。
  • 生产构建只经 tantivy ← module-search 带进 rayon;wasm 前端没有;image 一族只在 desktop feature 下进来。
  • 本站搜索走 tantivy 默认的单线程 executor,查询不碰 rayon。用到 rayon 的只有写入:每个 IndexWriter 一个 1 线程的 segment_updater 池和一个 4 线程的 merge_thread_* 池,都只用 spawn,结果经 oneshot 传回。
  • 本站写入时 tokio worker 阻塞在 tantivy 的 oneshot 和 census 的 Condvar 上,不是 rayon 的 LockLatch;元数据池没有 panic_handler,那里的 panic 会让整个进程 abort。

💬 关联讨论

发起讨论
加载中...