「Rayon → 并行流处理」流式处理

1,202 阅读4分钟

「这是我参与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
}