两个 trait
主线位置:主线 2 的入口。
(0..n).into_par_iter().sum()里,into_par_iter来自IntoParallelIterator,sum是ParallelIterator的一个默认方法,它造一个消费者交给drive_unindexed;Range<usize>是 indexed 的,于是drive_unindexed转身调bridge,再经with_producer拿到生产者。下一步见 plumbing。
flowchart TB into["IntoParallelIterator::into_par_iter"] --> pi["ParallelIterator(61 个方法)"] ref["IntoParallelRefIterator::par_iter = (&self).into_par_iter()"] --> into pi --> du["必须实现:drive_unindexed(UnindexedConsumer)"] pi --> ol["可选:opt_len()"] ipi["IndexedParallelIterator(34 个方法)"] -->|继承| pi ipi --> len["必须实现:len()"] ipi --> drv["必须实现:drive(Consumer)"] ipi --> wp["必须实现:with_producer(ProducerCallback)"] du -->|"消费者往下推"| leafc["Folder 在叶子上顺序消费"] wp -->|"生产者往上拉"| split["bridge 切分 Producer"]
三个入口 trait
IntoParallelIterator 对应 std 的 IntoIterator:
pub trait IntoParallelIterator {
type Iter: ParallelIterator<Item = Self::Item>;
type Item: Send;
fn into_par_iter(self) -> Self::Iter;
}
par_iter() 和 par_iter_mut() 不是另外实现的,而是两个 blanket impl(mod.rs:288、332):只要 &'data I: IntoParallelIterator,I 就有 par_iter(),函数体就一行 self.into_par_iter()。所以 v.par_iter() 等于 (&v).into_par_iter(),Vec<T>、[T]、HashMap 们只需要给自己的引用类型实现 IntoParallelIterator。
Item: Send 是第一道约束:元素要被送到别的线程上处理。ParallelIterator 本身还要求 Sized + Send(mod.rs:357),迭代器整体会被拆开分给不同线程。闭包参数普遍是 Fn + Sync + Send:同一个 map_op 被所有线程共享引用着调用,不能是 FnMut。
ParallelIterator:61 个方法,必须实现 1 个
trait ParallelIterator 从 357 行开始,到 2422 行结束,用 ^ fn 数是 61 个方法。去掉文档以后,必须实现的只有一个:
fn drive_unindexed<C>(self, consumer: C) -> C::Result
where
C: UnindexedConsumer<Self::Item>;
fn opt_len(&self) -> Option<usize> {
None
}
(mod.rs:2401、2419。)其余 60 个都有默认实现,形状只有两种:
- 适配器返回一个新结构体,什么都不做:
map就是Map::new(self, map_op),filter、fold、flat_map同理。和 std 一样是惰性的,结构体上都标着#[must_use = "iterator adaptors are lazy ..."]。 - 终结方法造一个消费者,调
self.drive_unindexed(consumer):sum是sum::sum(self),里面就一行pi.drive_unindexed(SumConsumer::new());count是self.map(one).sum()(mod.rs:571);find_any、reduce、for_each各有一个消费者。
opt_len 是给 collect 用的:返回 Some(len) 表示「我保证恰好产出 len 个元素」,Vec 的 par_extend 据此走就地写入的快路径(见 collect 到 Vec)。默认 None。indexed 的迭代器都返回 Some(self.len());filter、flat_map 不覆盖,就是 None。
IndexedParallelIterator:再加 3 个必须实现的
trait IndexedParallelIterator: ParallelIterator 有 34 个方法,必须实现三个:
fn len(&self) -> usize;
fn drive<C: Consumer<Self::Item>>(self, consumer: C) -> C::Result;
fn with_producer<CB: ProducerCallback<Self::Item>>(self, callback: CB) -> CB::Output;
(mod.rs:3211、3227、3244。)
「indexed」的意思是:长度事先确定,而且能在任意下标处切成两半。切片、Vec、Range<usize>、map / zip / enumerate 之后的链都是 indexed;filter 之后长度未知,就退化成只有 ParallelIterator。只有 indexed 的迭代器才有 zip、enumerate、rev、skip、take、chunks、collect_into_vec、with_min_len / with_max_len 这些方法,因为它们都要「按下标」做事。
drive 与 drive_unindexed 的区别只在消费者的类型:Consumer 能 split_at(index) 按下标切,UnindexedConsumer 只能 split_off_left() 不带下标地切。Range<usize> 的两个都直接调 bridge(range.rs:158-177)。
Range<u64> 在 rayon 里不是 indexed:u64、i64、u128、i128 的区间用的是 unindexed_range_impl!,因为长度可能超过 usize。但它的 drive_unindexed 先看 opt_len():长度放得进 usize,就改成 (0..len).into_par_iter().map(offset(start)).drive(consumer),注释写的是「Drive this in indexed mode for better collect」。所以 (0..n as u64).into_par_iter().collect() 也能就地写入,只是 zip 之类的方法用不了。
两种驱动:推和拉
一条链 a.map(f).filter(p).sum() 被驱动时,信息朝两个方向走:
- 消费者往下推:
sum造SumConsumer,交给Filter::drive_unindexed;Filter把它包成FilterConsumer,交给Map::drive_unindexed;Map再包一层MapConsumer交给源头。适配器拿到的是「下游要什么」,它只管在下游外面套一层。大部分适配器(map、filter、inspect、flat_map、fold……)都只用这一种方式,见Map::drive_unindexed:两行。 - 生产者往上拉:源头(切片、区间)收到消费者以后,调
bridge(self, consumer),bridge再调self.with_producer(callback)把自己变成一个Producer。如果中间有zip、enumerate这类适配器,它们的with_producer先向上游要生产者,包一层再交给回调。
为什么要两种?只用消费者,zip 写不出来:zip 需要把两个上游在同一个下标切开,消费者只代表「下游怎么接」,不知道上游的数据怎么切。enumerate 需要知道每一片的起始下标,rev 需要从后往前切,同样要直接操作生产者。所以 Zip::drive_unindexed 不是把消费者往下传,而是直接 bridge(self, consumer),在自己这一层把两个上游的生产者合成一个 ZipProducer,见 适配器选讲。
| 消费者(推) | 生产者(拉) | |
|---|---|---|
| 代表「什么」 | 下游怎么接收、怎么合并结果 | 上游的数据、怎么切 |
| 切分方法 | Consumer::split_at(index) / UnindexedConsumer::split_off_left() | Producer::split_at(index) / UnindexedProducer::split() |
| 叶子上 | into_folder() 变成 Folder,逐个 consume | fold_with(folder) 把自己的元素喂给 folder |
| 回溯时 | Reducer::reduce(left, right) | 无 |
| 谁实现 | 终结方法和大部分适配器 | 源头和 zip / enumerate / rev / chain / skip / take 等 |
with_producer 为什么是回调
自然的写法是给 IndexedParallelIterator 加一个关联类型 type Producer 和 fn into_producer(self) -> Self::Producer。rayon 没这么写,而是:
pub trait ProducerCallback<T> {
type Output;
fn callback<P>(self, producer: P) -> Self::Output
where
P: Producer<Item = T>;
}
(plumbing/mod.rs:17。)原因在 Map::with_producer 里:
fn with_producer<CB>(self, callback: CB) -> CB::Output
where
CB: ProducerCallback<Self::Item>,
{
return self.base.with_producer(Callback {
callback,
map_op: self.map_op,
});
struct Callback<CB, F> {
callback: CB,
map_op: F,
}
impl<T, F, R, CB> ProducerCallback<T> for Callback<CB, F>
where
CB: ProducerCallback<R>,
F: Fn(T) -> R + Sync,
R: Send,
{
type Output = CB::Output;
fn callback<P>(self, base: P) -> CB::Output
where
P: Producer<Item = T>,
{
let producer = MapProducer {
base,
map_op: &self.map_op,
};
self.callback.callback(producer)
}
}
}
MapProducer<'f, P, F> 里存的是 &'f F,借的是 callback() 这一帧栈上的 self.map_op。生产者要被切成成百上千份分给各个线程,每份都带一个 &F 最便宜;但这个 'f 是 callback 函数体里的生命周期,在 impl 层面写不出来,所以没法写 type Producer = MapProducer<'?, ...>。改成「我把生产者交给你」的回调,生产者的类型和生命周期都只在回调内部出现,trait 签名里不用出现生产者类型。
为什么不用 FnOnce(P)?因为回调必须对所有 P: Producer 泛型,而闭包不能是泛型的。所以每个适配器都要手写一个 struct Callback 加一个 impl ProducerCallback,上面那段代码有一半是样板。上游的 plumbing/README.md 有一节「What on earth is ProducerCallback?」讲同一件事(README 是资料,上面的说法已对照 map.rs 的代码核实)。
回调还有一个好处:with_producer 可以在自己的栈帧上准备并行期间要用的资源,把引用放进生产者,不用返回它。par_bridge 的 Mutex 和 threads_started 数组就是这样放在 drive_unindexed 的栈上、以引用交给生产者的。
和 std Iterator 对照
std::iter::Iterator | ParallelIterator | |
|---|---|---|
| 核心方法 | next():调用方一个一个拉 | drive_unindexed(consumer):迭代器把元素推给消费者 |
| 能否外部逐个取 | 能 | 不能,没有 next() |
| 闭包 | FnMut | Fn + Sync + Send |
| 长度 | size_hint 是提示 | opt_len / len 是承诺,collect 靠它写裸指针(写错会触发断言,见 collect 到 Vec) |
| 顺序 | 天然有序 | collect 保序(filter 之后的链也保序,par_bridge 除外);for_each 的执行顺序不确定;find_any 返回哪一个不确定 |
| 短路 | break / ? | 消费者的 full() 返回 true,各线程在下一次检查时停下 |
没有 next() 是设计上的选择:「下一个」在并行里没有意义。代价是想把一个只有 next() 的东西并行化,只能经过 par_bridge 加锁地一个个取。
小结
par_iter()=(&self).into_par_iter(),靠两个 blanket impl;集合类型只需给引用实现IntoParallelIterator。ParallelIterator61 个方法,必须实现drive_unindexed,可选opt_len;IndexedParallelIterator34 个方法,必须实现len/drive/with_producer。- 适配器返回结构体,终结方法造消费者调
drive_unindexed。大部分适配器只包消费者;zip/enumerate/rev这类必须拿到生产者。 with_producer写成ProducerCallback回调,是因为生产者借用了回调栈帧上的数据,类型里的生命周期在impl层写不出来,闭包又不能对生产者类型泛型。Range<u64>不是 indexed,但长度放得进usize时内部按 indexed 驱动,collect仍能就地写入。