rayon

C# 语言有一个很厉害的PLinq扩展,可以轻松地将linq语句并行化。比如:

string[] words = new[] {
    "Welcome",
    "to",
    "Beijing",
    "OK",
    "Hua",
    "Ying",
    "Ni",
    "2008"
};
var lazyBeeQuery = from word in words.AsParallel() select word;
lazyBeeQuery.ForAll < string > (word = >{
    Console.WriteLine(word);
});

在 新 的C++17 中,标准库也支持了 一 些并行算法:

 std: :experimental: :parallel: :for_each(std: :experimental: :parallel: :par, // 并行执行
v.begin(), v.end(), functor);

在Rust中,迭代器基本已经与linq的功能差不多。那我们能不能做个类似的扩展,让 普通迭代器轻松变成并行迭代器呢? Rayon的设计目标就是这个。

Rayon 是Rust 核心组成员Nicholas Matsakis 开发的一个并行迭代器项目。它可以把一个 按顺序执行的任务轻松变成并行执行。它非常轻量级,效率极高,而且使用非常简单。而且 它保证了无数据竞争的线程安全。

Rayon的 API 主要有两种:

  • 并行迭代器——对一个可迭代的序列调用par_iter 方法,就可以产生一个并行迭 代器;
  • join 函数——它可以把一个递归的分治算法问题变成并行执行。

照例,我们用示例来说明它的基本用法。比如,我们想对一个整数数组执行平方和计 算,可以这样做:

use rayon: :prelude: :*;
fn sum_of_squares(input: &[i32]) - >i32 {
    input.par_iter() //iter() 换成 par_iter()
    .map( | i | i * i).sum()
}

这个问题是可以并行计算的,每个元素的平方操作互不干扰,如果能让它们在不同线程 计算,最后再一起求和,可以提高执行效率。用Rayon来解决这个问题很简单,只需将单线 程情况下的iter() 方法改为par_iter() 即可。Rayon 会在后台启动一个线程池,自动 分配任务,将多个元素的map 操作分配到不同的线程中并行执行,最后把所有的执行结果汇 总再相加。

类似的,这个迭代器也有mut 版本。假如我们想并行修改某个数组,可以这样做:

use rayon: :prelude: :*;
fn increment_all(input: &mut[i32]) {
    input.par_iter_mut().for_each( | pl * p += 1);
}

Rayon 的另外一种使用方式是调用join 函数。这个函数特别适合于分治算法。一个典 型的例子是写一个快速排序算法:

fn partition < T: PartialOrd + Send > (v: &mut[T]) - >usize {
    let pivot = v.len() - 1;
    let mut i = 0;
    forj in 0..pivot {
        if v[j] <= v[pivot] {
            V.swap(i, j);
            i += 1;
        }
    }
    V.swap(i, pivot);
    i
}
fn quick_sort < T: Partial0rd + Send > (V: &mut[T]) {
    if v.len() <= 1 {
        return;
    }
    let mid = partition(V);
    let(lo, hi) = v.split_at_mut(mid);
    rayon: :join( | l quick_sort(lo), Il quick_sort(hi));
}
fn main() {
    let mut v = vec ! [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
    quick_sort( & mut v);
    println ! ("{:?}", v);
}

在快速排序算法中,我们可以先把数组切分为两部分,然后分别再对这两部分执行快速 排序。在这里,我们使用了rayon::join函数。

需要注意的是,并行迭代器和join 函数并不是简单地新建线程,然后在两个线程上 分别执行。它内部实际上使用了 “work steal” 策略。它后台的线程是由一个线程池管理的, join函数只是把这两个闭包作为两个任务分发出去了,并不保证这两个闭包一定会并行执 行或者串行执行。如果现在有空闲线程,那么空闲线程就会执行这个任务。总之,哪个线程 有空,就在哪个线程上执行,它不会让某些线程早早结束而让某些任务在其他线程里面等 待。所以,它的开销是非常小的。 Rayon 这个库在性能测试的benchmark 上的表现也是非常 不错的,具体数据大家可以查看官方网站,或者自行测试。

同时,我还要强调一点,Rayon 同样保证了“线程安全”。比如,我们如果想在两个任 务中同时修改一个数组,编译器会阻止我们:

fn increment_all(slice: &mut[i32]) {
    rayon: :join( | l process(slice), Il process(slice));
}

我们应该能猜想到,这里的API 肯定用到了Send 、Sync 之类的约束,就跟标准库中 的 spawn 函数一样。因此第三方库也一样能享受到“线程安全”的优点。

有关这个库的使用方法以及其内部实现原理,在Nicholas Matsakis的博客有详细描述, 本书篇幅有限就不再展开了。从这个库我们可以看到,Rust 为各种并行开发的模式提供了无 限的可能性。虽然标准库在这方面提供的直接选择不多,但并没有阻碍我们实现各种各样的 第三方库。Rust 在并行开发方面同时实现了执行效率高、安全性好、扩展性好的特点。


本书中的绝大部分代码示例都是很短的, 一个文件就可以搞定。但是,任何一个规模稍 微大一点的项目都不能这么写。我们需要一个机制,把一个项目切分成若干小部分,每个部 分又可以切分成更小的部分,层层抽象,通过这种方式来管理复杂的代码。这就是很多编程 语言中都有的“模块系统”。

Rust 用了两个概念来管理项目: 一个是crate, 一个是mod。

  • crate 简单理解就是一个项目。crate 是 Rust 中的独立编译单元。每个crate 对应生成一 个库或者可执行文件(如.lib .dll .so exe等)。官方有一个 crate 仓库https://crates.io/, 可以供用户发布各种各样的库,用户也可以直接使用这里面的开源库。
  • mod 简单理解就是命名空间。mod 可以嵌套,还可以控制内部元素的可见性。
    crate 和 mod 有一个重要区别是: crate 之间不能出现循环引用;而mod 是无所谓的, mod1 要使用mod2 的内容,同时mod2 要使用mod1 的内容,是完全没问题的。在Rust 里 面 ,crate 才是一个完整的编译单元(compile unit)。也就是说,rustc 编译器必须把整个crate 的内容全部读进去才能执行编译, rustc 不是基于单个的.rs 文件或者mod 来执行编译的。作 为对比,C/C++ 里面的编译单元是单独的.c/.cpp 文件以及它们所有的include 文件。每个.c/. cpp 文件都是单独编译,生成.o文件,再把这些.0文件链接起来。

cargo

Cargo是 Rust 的包管理工具,是随着编译器一起发布的。在使用rustup安装了官方发布 的 Rust 开发套装之后, Cargo 工具就已经安装好了,无须单独安装。我们可以使用cargo命令来查看它的基本用法。 Cargo 的官方使用文档在这个地址: https://doc.rust-lang.org/ cargo/。
Cargo 可以用于创建和管理项目、编译、执行、测试、管理外部下载的包和可执行文 件 等 。

下面我们使用cargo 来创建一个项目,一步步带领大家学习cargo 的基本用法。
我们创建一个新的工程,这个工程会生成一个可执行程序。步骤如下。
(1)进入项目文件夹后,使用如下命令:

cargo   new   hello_world   --bin

后面的—-bin 选项意味着我们希望生成的是可执行程序, cargo 会帮助我们生成一个 main.rs 的模板文件。
如果我们的工程是一个library, 则可以使用–lib 选项,此时自动生成的是一个lib. rs 的模板文件。

( 2 ) 使 用tree .命令查看当前的文件夹结构。可以看到:

2 directories,2 files

其中,Cargo.toml 是我们的项目管理配置文件,这里记录了该项目相关的元信息。 关于这个文件的详细格式定义,可以参考官方网站上的帮助文档: https://doc.rust-lang.org/ cargo/。

src 文件夹内是源代码。
( 3 ) 进 入hello_world 文件夹,使用cargo build命令,编译项目。生成的可执 行文件在./target/debug/ 文件夹内。如果我们使用cargo build --release命令, 则可以生成release 版的可执行文件,它比debug 版本优化得更好。

(4)使用 . /target/debug/hello_world 命令,或者 cargo run命令,可以执行 我们刚生成的这个可执行程序。
在 Rust 中,一个项目对应着一个目标文件,可能是library, 也可能是可执行程序。现在 我们试试给我们的程序添加依赖项目。
进入hello_world 的上一层文件夹,新建一个library 项目:

cargo new  good_bye

lib.rs 文件是库项目的入口,打开这个文件,写入以下代码:

pub fn say(){
  println!("good bye");
}

使用cargo build,编译通过。现在我们希望hello_world 项目能引用good_bye 项目。打开hello_world 项目的Cargo.toml 文件,在依赖项下面添加对good_bye 的 引用 。

[dependencies]
good_bye ={path ="../good_bye"}

这个写法是引用本地路径中的库。如果要引用官方仓库中的库更简单,比如:

[dependencies]
lazy_static ="1.0.0"

现在在应用程序中调用这个库。打开main.rs源文件,修改代码为:

extern crate good_bye;
fn main(){
println!("Hello,world!");
good_bye::say(); }

再次使用cargo run 编译执行,就可以看到我们正确调用了good_bye 项目中的 代码。

cargo 只是一个包管理工具,并不是编译器。Rust的编译器是rustc,使 用cargo编译工 程实际上最后还是调用的rustc来完成的。如果我们想知道cargo在后面是如何调用rustc完 成编译的,可以使用cargo build --verbose选项查看详细的编译命令。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐