并行迭代器
rayon 本体(不含 rayon-core)里没有一个线程,也没有一个队列。par_iter() 后面那一长串组合子,最后都会被翻译成一棵二分递归:每一层调用一次 join_context,左右两半各自继续切,切不动了就在当前线程上顺序跑完这一段。切到第几层停,由 plumbing 里的 Splitter 根据线程数和「这个任务是不是被偷来的」临时决定。
这一部分沿着主线 2「一次 par_iter().sum()」读 rayon 的 src/iter/,讲七件事:
- 两个 trait:
ParallelIterator61 个方法只有drive_unindexed必须实现;IndexedParallelIterator34 个方法里len/drive/with_producer必须实现。为什么with_producer要写成回调。 - plumbing:
Producer、Consumer、Folder、Reducer和两个 unindexed 变体;以map为例拼出一个完整的包装;full()怎么让find_any短路。 - bridge 与自适应切分:本部分的核心章。
Splitter的三行规则;从池外调用多切一层(12 线程最少 64 片,install里最少 16 片);with_min_len/with_max_len改变什么(E2、E4)。 - 适配器选讲:
map、filter、flat_map、zip、enumerate、chain各自怎么包,其余列一张总表。 - collect 到 Vec:indexed 链一次分配、各片写进不相交的区间;
filter链走LinkedList<Vec<T>>再拼接(E8:1 次分配对数千次)。 - fold、reduce 与 sum:每个叶子一个累加值;identity 会被调用很多次;
f32求和结果随切分变化(E8)。 - 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.rs | 3628 | 两个 trait 的定义与文档、IntoParallelIterator 家族、FromParallelIterator / ParallelExtend |
plumbing/mod.rs | 476 | Producer / Consumer / Folder / Reducer、Splitter / LengthSplitter、bridge 三个函数 |
plumbing/README.md | 315 | 上游写的说明,是资料,本部分以代码为准 |
collect/ | 667 | collect_into_vec、special_extend、CollectConsumer |
extend.rs | 619 | 各集合的 ParallelExtend;fast_collect、ListVecConsumer |
par_bridge.rs | 157 | ParallelBridge |
map.rs、filter.rs、zip.rs、enumerate.rs、chain.rs、flat_map.rs | 100–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,页面上写出两者。计数类实验(叶子数、分配次数)不受负载影响的部分在多种负载下都跑过,受窃取影响的部分注明了负载。
| 编号 | 目录 | 内容 | 页面 |
|---|---|---|---|
| E2 | split-count/ | 自写 Producer 计 split_at / fold_with 次数,与 fold().count() 对照;池外 vs 池内、1 / 2 / 4 / 12 线程、with_min_len / with_max_len | splitter |
| E4 | min-max-len/ | with_min_len / with_max_len 对耗时的影响,轻 / 重两档工作量 | splitter |
| E8 | collect-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。
💬 关联讨论
发起讨论加载中...