简单的并发

Rust 的一个承诺是实现无畏并发。很自然的,Rust支持通过许多机制编写并发代码。在本章中,我们将讨论其中的一些。我们已经看到Rust编译器如何使用借用检查来确保编译时程序的正确性。事实证明,这些原语在验证并发代码的正确性方面也很有用。现在,有多种方法可以在一种语言中实现线程。最简单的方法是为平台中创建的每个线程创建一个新的 OS 线程。 这通常称为 1:1 线程。另一方面,许多应用程序线程可以映射到一个OS 线程。这称为 N:1 线程。虽然这种方法很轻松,因为我们最终实际线程较少,但上下文切换的开销较高。中间层称为 M:N 线程,其中多个应用程序线程映射到多个OS级别线程。 这种方法需要最大程度的安全保护,并使用运行时实现,这是 Rust 避免的。因此,Rust使用 1:1 模型。与 Go 等语言相比,Rust中的一个线程对应一个 OS 线程。让我们先看看 Rust 如何编写多线程应用程序:

// code/chapter2/threads.rs

use std::thread;

fn main() {
    for i in 1..10 {
        let handle = thread::spawn(move || {
            println!("Hello from thread number {}", i);
        });
        let _ = handle.join();
    }
}

我们引入线程。在 main 函数中,我们创建一个空向量,用它来存储创建的线程的引用,以便我们可以等待它们退出。线程实际际上是使用 thread::spawn 创建的,我们必须传递一个将每个线程中执行的闭包。因为我们必须在闭包中从封闭作用域(循环索引i)中借用一个变量,所以闭包本身必须是一个 move 闭包。在退出闭包之前,我们调用当前线程句柄的连接,以便所有线程彼此等待。这会产生以下输出:

# rustc threads.rs && ./threads 
Hello from thread number 1 
Hello from thread number 2 
Hello from thread number 3 
Hello from thread number 4 
Hello from thread number 5 
Hello from thread number 6 
Hello from thread number 7 
Hello from thread number 8 
Hello from thread number 9

多线程应用程序的真正强大之处在于线程可以合作进行有意义的工作。为此,有两件重要的事情是必要的。线程需要能够从彼此传递数据,并且应该有方法来协调线程的调度方式,以便它们不会相互跨越。对于第一个问题,Rust提供了一条消息,通过通道传递机制。我们来看下面的例子:

// code/chapter2/channels.rs
use std::sync::mpsc;
use std::thread;

fn main() {
    let rhs = vec![10, 20, 30, 40, 50, 60, 70];
    let lhs = vec![1, 2, 3, 4, 5, 6, 7];
    let (tx, rx) = mpsc::channel();

    assert_eq!(rhs.len(), lhs.len());
    for i in 1..rhs.len() {
        let rhs = rhs.clone();
        let lhs = lhs.clone();
        let tx = tx.clone();
        let handle = thread::spawn(move || {
            let s = format!(
                "Thread {} added {} and {}, result {}",
                i,
                rhs[i],
                lhs[i],
                rhs[i] + lhs[i]
            );
            tx.clone().send(s).unwrap();
        });

        let _ = handle.join().unwrap();
    }

    drop(tx);

    for result in rx {
        println!("{}", result);
    }
}

这个例子很像前一个例子。我们导入必要的模块以便能够使用频道。我们定义了两个向量,我们将为两个向量中的每对元素创建一个线程,以便我们可以添加它们并返回结果。我们创建了通道,它将句柄返回给发送端和接收端。作为安全检查,我们确保两个矢量确实具有相同的长度。然后,我们继续创建我们的线程。由于我们需要在这里访问外部变量,因此线程需要像上一个示例那样接受移动闭包。此外,编译器将尝试使用 Copy trait 将这些变量复制到线程。在这种情况下,由于矢量类型没有实现Copy,因此会失败。

我们需要显式 clone 资源,以便不需要复制它们。我们运行计算并将结果发送到管道的发送端。后来,我们加入了所有的主题。在我们遍历接收端并打印结果之前,我们需要显式删除对发送端的原始句柄的引用,以便在我们开始接收之前销毁所有发送者(当线程退出时,克隆的发送者将被自动销毁) 。这将按预期打印以下内容:

# rustc channels.rs && ./channels 
Thread 1 added 20 and 2, result 22 
Thread 2 added 30 and 3, result 33 
Thread 3 added 40 and 4, result 44 
Thread 4 added 50 and 5, result 55 
Thread 5 added 60 and 6, result 66 
Thread 6 added 70 and 7, result 77

另请注意,mpsc代表多个生产者单一消费者。

在处理多个线程时,另一个常见的习惯用法是在所有线程之间共享一个公共状态。然而,在许多情况下,这可能是一罐蠕虫。调用者需要仔细设置排除机制,以便以 race-free 无种族的方式共享状态。幸运的是,借阅检查器可以帮助确保这更容易。Rust有许多智能指针用于处理共享状态。该库还提供了一个通用的互斥类型,可以在处理多个线程时用作锁。但也许最重要的是发送和同步特性。实现发送特性的任何类型都可以在多个线程之间安全地共享。同步特性表示多个线程对给定数据的访问是安全的。关于这些特征有一些规则:

  • 所有内置类型都实现 SendSync ,除了任何 unsafe 的东西,一些智能指针类型,如 Rc<T>UnsafeCell<T>
  • 复合类型将自动实现两者,只要它没有任何不实现 SendSync 的类型

std::sync 包有很多类型和帮助器来处理并行代码。

在上一段中,我们提到了不安全的 Rust 。让我们绕道走,再仔细看看。Rust 编译器通过使用一个健壮的类型系统为安全编程提供了一些强有力的保证。然而,在某些情况下,这些可能会成为更大的开销。为了处理这种情况,语言提供了一种选择退出这些保证的方法。用不安全关键字标记的代码块可以做 Rust 可以做的所有事情,以及以下内容:

  • 取消引用原始指针类型(*mut 或者 *const T)
  • 调用不安全的函数或者方法
  • 实现标记为不安全的特性。
  • 改变一个静态变量

让我们看一个使用 unsafe 代码块取消引用指针的示例:

// code/chapter2/unsafe.rs

fn main() {
    let num: u32 = 43;
    let p: *const u32 = &num;

    unsafe {
        assert_eq!(*p, num);
    }
}

在这里,我们创建一个变量和一个指向它的指针;如果我们尝试在不使用 unsafe 块的情况下取消引用指针,编译器将拒绝编译。在不安全的块内,我们在取消引用时返回原始值。虽然不安全的代码可能很危险,但它在低级编程(如内核(RedoxOS)和嵌入式系统)中非常有用。