Rust in Everything
基于 rayon 1.12.0

并行迭代器

rayon 本体(不含 rayon-core)里没有一个线程,也没有一个队列。par_iter() 后面那一长串组合子,最后都会被翻译成一棵二分递归:每一层调用一次 join_context,左右两半各自继续切,切不动了就在当前线程上顺序跑完这一段。切到第几层停,由 plumbing 里的 Splitter 根据线程数和「这个任务是不是被偷来的」临时决定。

这一部分沿着主线 2「一次 par_iter().sum()」读 rayon 的 src/iter/,讲七件事:

  1. 两个 trait:ParallelIterator 61 个方法只有 drive_unindexed 必须实现;IndexedParallelIterator 34 个方法里 len / drive / with_producer 必须实现。为什么 with_producer 要写成回调。
  2. plumbing:Producer、Consumer、Folder、Reducer 和两个 unindexed 变体;以 map 为例拼出一个完整的包装;full() 怎么让 find_any 短路。
  3. bridge 与自适应切分:本部分的核心章。Splitter 的三行规则;从池外调用多切一层(12 线程最少 64 片,install 里最少 16 片);with_min_len / with_max_len 改变什么(E2、E4)。
  4. 适配器选讲:map、filter、flat_map、zip、enumerate、chain 各自怎么包,其余列一张总表。
  5. collect 到 Vec:indexed 链一次分配、各片写进不相交的区间;filter 链走 LinkedList<Vec<T>> 再拼接(E8:1 次分配对数千次)。
  6. fold、reduce 与 sum:每个叶子一个累加值;identity 会被调用很多次;f32 求和结果随切分变化(E8)。
  7. par_bridge:把顺序 Iterator 接进来,最多切出线程数 + 1 个叶子,每个元素一次加锁;防递归死锁的那个标志位。
flowchart TB
  user["v.par_iter().map(f).sum()"] --> sum["sum:drive_unindexed(SumConsumer)"]
  sum --> mapc["Map:把 consumer 包成 MapConsumer,往下传"]
  mapc --> src["切片 Iter:bridge(self, consumer)"]
  src --> wp["with_producer:拿到 IterProducer"]
  wp --> bpc["bridge_producer_consumer:LengthSplitter"]
  bpc --> q{"try_split?"}
  q -->|是| jc["split_at 两边 + join_context"]
  jc --> bpc
  q -->|否| leaf["fold_with(consumer.into_folder()):顺序跑完这一段"]
  leaf --> red["回溯时 reducer.reduce(左, 右)"]
  jc -.->|"rayon-core"| core["registry::in_worker → 本地 deque / 注入队列"]

文件地图

src/iter/ 在 v1.12.0 共 18797 行(含文档注释和测试),本部分读到的文件:

文件行数内容
mod.rs3628两个 trait 的定义与文档、IntoParallelIterator 家族、FromParallelIterator / ParallelExtend
plumbing/mod.rs476Producer / Consumer / Folder / Reducer、Splitter / LengthSplitter、bridge 三个函数
plumbing/README.md315上游写的说明,是资料,本部分以代码为准
collect/667collect_into_vec、special_extend、CollectConsumer
extend.rs619各集合的 ParallelExtend;fast_collect、ListVecConsumer
par_bridge.rs157ParallelBridge
map.rs、filter.rs、zip.rs、enumerate.rs、chain.rs、flat_map.rs100–300每个适配器一个文件
fold.rs、reduce.rs、sum.rs、try_fold.rs、try_reduce.rs—归约类消费者
len.rs—MinLen / MaxLen:with_min_len / with_max_len

实验

本部分的实验在 exp-rayon/ 下,模板是 exp-rayon/base(rayon =1.12.0、rayon-core =1.13.0),release 构建。本机 M2 Max,8 性能核 + 4 能效核,共 12 个逻辑核,macOS 不能绑核;计时类数字每组至少 5 次取中位数,测前 1 分钟和 5 分钟负载都低于 3,页面上写出两者。计数类实验(叶子数、分配次数)不受负载影响的部分在多种负载下都跑过,受窃取影响的部分注明了负载。

编号目录内容页面
E2split-count/自写 Producer 计 split_at / fold_with 次数,与 fold().count() 对照;池外 vs 池内、1 / 2 / 4 / 12 线程、with_min_len / with_max_lensplitter
E4min-max-len/with_min_len / with_max_len 对耗时的影响,轻 / 重两档工作量splitter
E8collect-paths/indexed 与 filter 两条 collect 路径的分配次数和耗时;collect_into_vec 不换缓冲区;谎报 opt_len 时的两条断言;f32 求和的结果个数collect、fold-reduce
—par-bridge/par_bridge 的叶子数、顺序、每元素开销,对照 par_iter 和「先 collect 再 par_iter」par-bridge

只想会用的读者,读 两个 trait、bridge 与自适应切分 和 collect 到 Vec;想自己写并行迭代器的,从 plumbing 开始按顺序读。主线 2 的全程时序图在一次 par_iter;每一层 join_context 内部发生什么,见 join 与 join_context。

💬 关联讨论

发起讨论
加载中...