「这是我参与11月更文挑战的第 22 天,活动详情查看:2021最后一次更文挑战」
为了使 avg_values_jsonl 并行化,我们不能像对 sum_of_squares() 那样将 par_iter()添加到组合中。 par_iter()只为具体的容器类型定义,因为它依赖于知道容器中项目的数量并能将它们分成子部分来操作,这不适用于通用迭代器。
然而Rayon专门为流式数据的使用情况提供了一个不同的机制,一个定义在具有相应trait的迭代器上的 par_bridge()。
该方法将一个普通的迭代器调整为一个并行的迭代器,采用了巧妙的设计,从迭代器中获取单个项目,并在Rayon的线程中平衡它们。最重要的是,它实现了 ParallelIterator,这意味着你可以完全像在一个集合上使用 par_iter() 返回的值那样来使用这个适配器。
让我们试着在迭代链中插入par_bridge():
use rayon::prelude::*;
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)
.par_bridge() // this is new
.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
}
不出所料,我们的第一次尝试没有编译成功。
Compiling playground v0.1.0 (/home/hniksic/work/playground)
error[E0599]: no method named `par_bridge` found for struct `std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>` in the current scope
--> src/main.rs:12:10
|
12 | .par_bridge()
| ^^^^^^^^^^ method not found in `std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>`
|
= note: the method `par_bridge` exists but the following trait bounds were not satisfied:
`std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: std::marker::Send`
which is required by `std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: rayon::iter::par_bridge::ParallelBridge`
`&std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: std::marker::Send`
which is required by `&std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: rayon::iter::par_bridge::ParallelBridge`
`&std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: std::iter::Iterator`
which is required by `&std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: rayon::iter::par_bridge::ParallelBridge`
`&mut std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: std::marker::Send`
which is required by `&mut std::iter::Map<std::io::Lines<std::io::BufReader<impl Read>>, fn(std::result::Result<std::string::String, std::io::Error>) -> std::string::String {std::result::Result::<std::string::String, std::io::Error>::unwrap}>: rayon::iter::par_bridge::ParallelBridge`
很迷,是不是?(但仍然没有Tokio那么糟糕,可以看看他们的吐糟,hhhh)。
错误信息有点难看明白的,因为我们试图调用 par_bridge() 的迭代器有一个复杂的通用类型,来自 map() 包裹 lines() 包裹 BufReader 包裹输入的通用类型。
实际的问题在 "注释" 中得到了解释,它说:"方法par_bridge存在,但以下特质的界限没有得到满足。: std::marker::Send"。
input.lines() 返回的迭代器没有实现Send,因为它包含了从input移动的值,而input的类型只知道实现了Read trait。没有Send,Rayon就没有权限将迭代器发送到另一个线程,而它可能需要这么做。
如果这个函数被允许以书面形式编译,那么用一个不属于Send的输入来调用它,也许是因为它包含Rc<_>或其他非Send类型,会使程序崩溃。幸运的是,Rustc防止了这种情况的发生,并且由于缺少绑定而拒绝了代码,即使错误信息可以更顺畅一些。
一旦我们理解了这个问题,解决方法很简单:在Read之外增加Send属性绑定,将 input 声明为input: impl Read + Send。有了这个改变,我们就会得到一个不同的编译错误。
error[E0594]: cannot assign to `cnt`, as it is a captured variable in a `Fn` closure
--> src/main.rs:17:13
|
17 | cnt += 1;
| ^^^^^^^^ cannot assign
这里的问题是,闭包会改变共享状态,即cnt计数器。
这就要求闭包通过唯一的(可变)引用来捕获cnt,这使得它成为一个FnMut闭包。这在单线程代码中完全没有问题,但Rayon计划从多个线程中调用该闭包,所以它要求一个Fn闭包。编译器拒绝了Fn闭包中的赋值,使我们避免了数据竞赛的潜在问题。这很RUST
这两个问题都不是Rayon所特有的,如果我们试图用其他方法将闭包传递给多个线程,我们也会遇到完全相同的错误。我们可以通过将cnt切换为AtomicUsize来解决赋值问题,这样可以通过共享引用安全地修改。
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::io::{BufRead, BufReader, Read};
use serde_json::{Map, Value};
fn avg_values_jsonl(input: impl Read + Send) -> f64 {
let input = BufReader::new(input);
let cnt = AtomicUsize::new(0);
let total: f64 = input
.lines()
.map(Result::unwrap)
.par_bridge()
.filter_map(|line| serde_json::from_str(&line).ok())
.filter_map(|obj: Map<String, Value>| obj.get("value").cloned())
.filter_map(|value| {
cnt.fetch_add(1, Ordering::Relaxed);
value.as_f64()
})
.sum();
total / cnt.into_inner() as f64
}