「这是我参与11月更文挑战的第 21 天,活动详情查看:2021最后一次更文挑战」
抽象单元
Rayon API提供了两个不同的抽象:Fork-join 和 并行迭代器。
fork-join是较低级别的单元,看似简单:它由一个单一的函数join()组成,它接受两个闭包并执行它们,可能是并行的。
rayon::join(A, B):将B推到线程本地队列中,并开始执行A —— 在同一个线程中。当A返回时,有可能B已经被另一个线程窃取了,在这种情况下,当前线程会等待它完成,在等待时从其他线程窃取任务。如果B没有被窃取,join()也只是执行了它,不会比代码的顺序性差。
Fork-join 优雅地映射到分而治之的算法中,但Rayon最出名的是它在fork-join之上建立的另一个抽象,即并行迭代器。
它们用一个 par_iter() 丰富了标准库中的集合,该方法返回一个 ParallelIterator,一个与Iterator非常相似的对象,但当它用完时,会并行地执行传递给 map() 和 fold() 等方法的闭包。简单地使用 rayon::prelude::* 就可以使用这个功能,然后为Vec或&[T]等容器添加par_iter()。
像在 sum_of_squares 的例子中那样,对一个slice进行调用,par_iter()将其划分为更小的块,在背后使用 rayon::join() 进行并行处理。
像sum()和更通用的fold()和reduce()方法,也是并行地汇总输出,最终返回一个单一的值。
ParallelIterator 语义的一个结果是,你不能只是在普通的for循环中使用它,你必须使用它自己提供的方法来耗尽它,比如已经提到的sum()、fold()和reduce(),还有for_each()和其他方法。传递给接受它们的方法的闭包必须可以从多个线程中调用,编译器会认真检查,我们将在下面看到。
Rayon的作者在 经典博文中 对其进行了出色的介绍,你一定要看看。
流式处理
在介绍Rayon时,经常被忽略的一个方面是对流数据的处理。当处理一个流时,我们没有一个方便的容器来划分,我们有的只有当前到达的 item,不知道它们总共有多少。这使得一些并行处理技术无法使用,所以我们必须以稍微不同的方式来做事情。
作为一个例子,让我们来处理一个以每行一条记录的jsonl格式的JSON编码的数值流。我们将计算每条记录中值域的平均值,顺序代码看起来像这样:
use std::io::{BufRead, BufReader, Read};
use serde_json::{Map, Value};
fn avg_values_jsonl(input: impl Read) -> f64 {
let input = BufReader::new(input);
let mut cnt = 0usize;
let total: f64 = input
.lines()
.map(Result::unwrap)
.filter_map(|line| serde_json::from_str(&line).ok())
.filter_map(|obj: Map<String, Value>| obj.get("value").cloned())
.filter_map(|value| {
cnt += 1;
value.as_f64()
})
.sum();
total / cnt as f64
}
足够清楚了吧 —— 遍历输入行,放弃那些我们不能反序列化为JSON对象的行,删除没有值的对象或那些值不是数字的对象,将它们相加,然后除以有效记录的数量。
这个函数对其输入的类型是通用的,所以我们可以用任何实现io::Read的输入来调用它,比如一个文件,由 io::stdin::lock() 返回的标准输入句柄,甚至是一个&[u8]。
fn main() {
let json = r#"{"value": 1} {"value": 15.3, "foo": "bar"} {} {"value": 100}"#
.as_bytes();
assert_eq!(avg_values_jsonl(json), 116.3 / 3.);
}