使用线程同时运行代码


在大多数当前的作系统中,已执行程序的代码在 进程,作系统将同时管理多个进程。在程序中,您还可以拥有同时运行的独立部分。运行这些独立部分的功能称为线程。例如,Web 服务器可以有多个线程,以便它可以同时响应多个请求。


将程序中的计算拆分为多个线程以同时运行多个任务可以提高性能,但也会增加复杂性。由于线程可以同时运行,因此无法保证不同线程上的代码部分的运行顺序。这可能会导致问题,例如:


  • 争用条件,其中线程以不一致的顺序访问数据或资源

  • 死锁,其中两个线程相互等待,阻止两个线程继续

  • 仅在某些情况下发生且难以可靠地重现和修复的错误


Rust 试图减轻使用线程的负面影响,但在多线程上下文中编程仍然需要仔细考虑,并且需要与在单线程中运行的程序不同的代码结构。


编程语言以几种不同的方式实现线程,许多作系统都提供了语言可以调用的 API 来创建新线程。Rust 标准库使用 1:1 的线程实现模型,即程序对一个语言线程使用一个作系统线程。有些 crate 实现了其他线程模型,这些模型对 1:1 模型进行了不同的权衡。


使用 spawn 创建新线程


为了创建一个新线程,我们调用 thread::spawn 函数并给它传递一个闭包(我们在第 13 章中讨论了闭包),其中包含我们想要在新线程中运行的代码。示例 16-1 中的示例打印了主线程中的一些文本和新线程中的其他文本:


文件名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}


示例 16-1:创建一个新线程来打印一个东西,而主线程打印另一个东西


请注意,当 Rust 程序的主线程完成时,所有生成的线程都会关闭,无论它们是否已经完成运行。此程序的输出可能每次都略有不同,但看起来类似于以下内容:

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!


thread::sleep 的调用会强制线程在短时间内停止其执行,从而允许不同的线程运行。线程可能会轮流进行,但这并不能保证:这取决于您的作系统如何调度线程。在此运行中,主线程首先打印,即使来自生成的线程的 print 语句首先出现在代码中。即使我们告诉生成的线程打印到 i 为 9,但在主线程关闭之前它只打印到 5。


如果运行此代码并且只看到主线程的输出,或者没有看到任何重叠,请尝试增加范围中的数字,以便为作系统在线程之间切换创造更多机会。


等待所有线程完成使用联接句柄


示例 16-1 中的代码不仅由于主线程结束而在大多数时候提前停止了 spawned thread,而且因为无法保证线程的运行顺序,我们也不能保证 spawned thread 会运行!


我们可以通过将 thread::spawn 的返回值保存在变量中来解决 spawned thread 无法运行或提前结束的问题。返回类型 thread::spawnJoinHandleJoinHandle 是一个拥有的值,当我们对其调用 join 方法时,将等待其线程完成。示例 16-2 展示了如何使用我们在示例 16-1 中创建的线程的 JoinHandle 并调用 join 来确保生成的线程在 main 退出之前完成:


文件名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }

    handle.join().unwrap();
}


示例 16-2:从 thread::spawn 保存 JoinHandle 保证线程运行完成


对句柄调用 join 会阻止当前正在运行的线程,直到句柄表示的线程终止。阻塞线程意味着阻止线程执行工作或退出。因为我们在主线程的 for 循环之后调用了 join,所以运行示例 16-2 应该会产生类似于这样的输出:

hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!


两个线程继续交替,但主线程由于调用 handle.join() 而等待,并且在生成的线程完成之前不会结束。


但是,让我们看看当我们将 handle.join() 移动到 for 循环,如下所示


文件名: src/main.rs

use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..10 {
            println!("hi number {i} from the spawned thread!");
            thread::sleep(Duration::from_millis(1));
        }
    });

    handle.join().unwrap();

    for i in 1..5 {
        println!("hi number {i} from the main thread!");
        thread::sleep(Duration::from_millis(1));
    }
}


主线程将等待生成的线程完成,然后运行其 for 循环,因此输出将不再交错,如下所示:

hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!


小细节(例如调用 join 的位置)可能会影响线程是否同时运行。


move 闭包与线程一起使用


我们经常使用 move 关键字和传递给 thread::spawn 的闭包 因为 Closure 将从 environment 的 intent 中,从而将这些值的所有权从一个线程转移到 另一个。在第 13 章的 “捕获引用或移动所有权” 一节中,我们讨论了闭包上下文中的 move。现在,我们将更多地关注 movethread::spawn 之间的交互。


请注意,在示例 16-1 中,我们传递给 thread::spawn 的闭包没有参数:我们在生成的线程代码中没有使用来自主线程的任何数据。要在 spawned thread 中使用来自主线程的数据,spawned thread 的 closure 必须捕获它需要的值。示例 16-3 展示了在主线程中创建 vector 并在生成的线程中使用它的尝试。但是,这还不起作用,您稍后会看到。


文件名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}


示例 16-3:尝试在另一个线程中使用主线程创建的 vector


闭包使用 v,因此它将捕获 v 并使其成为闭包环境的一部分。因为 thread::spawn 在新线程中运行这个闭包,我们应该能够在该新线程中访问 v。但是当我们编译这个例子时,我们得到以下错误:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
 --> src/main.rs:6:32
  |
6 |     let handle = thread::spawn(|| {
  |                                ^^ may outlive borrowed value `v`
7 |         println!("Here's a vector: {v:?}");
  |                                     - `v` is borrowed here
  |
note: function requires argument type to outlive `'static`
 --> src/main.rs:6:18
  |
6 |       let handle = thread::spawn(|| {
  |  __________________^
7 | |         println!("Here's a vector: {v:?}");
8 | |     });
  | |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++

For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error


Rust 推断如何捕获 v,因为 println!只需要对 v 的引用,所以闭包会尝试借用 v。但是,有一个问题: Rust 无法判断生成的线程将运行多长时间,因此它不知道对 v 的引用是否始终有效。


示例 16-4 提供了一个更有可能引用 v 的场景 那将是无效的:


文件名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(|| {
        println!("Here's a vector: {v:?}");
    });

    drop(v); // oh no!

    handle.join().unwrap();
}


示例 16-4:一个带有闭包的线程,它试图从丢弃 v 的主线程中捕获对 v 的引用


如果 Rust 允许我们运行此代码,则生成的线程可能会立即置于后台,根本不会运行。生成的线程内部有对 v 的引用,但主线程立即丢弃 v,使用我们在第 15 章中讨论的 drop 函数。然后,当生成的线程开始执行时, v 不再有效,因此对它的引用也无效。哦不!


要修复示例 16-3 中的编译器错误,我们可以使用错误消息的建议:

help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
  |
6 |     let handle = thread::spawn(move || {
  |                                ++++


通过在闭包之前添加 move 关键字,我们强制闭包获得它正在使用的值的所有权,而不是让 Rust 推断它应该借用这些值。示例 16-5 中所示的对示例 16-3 的修改将按照我们的预期编译和运行:


文件名: src/main.rs

use std::thread;

fn main() {
    let v = vec![1, 2, 3];

    let handle = thread::spawn(move || {
        println!("Here's a vector: {v:?}");
    });

    handle.join().unwrap();
}


示例 16-5:使用 move 关键字强制闭包获得它使用的值的所有权


我们可能很想尝试同样的事情来修复示例 16-4 中的代码,其中主线程使用 move 闭包调用 drop。然而,这个修复是行不通的,因为示例 16-4 试图做的事情由于不同的原因而被禁止。如果我们在闭包中添加 move,我们会将 v 移动到闭包的环境中,并且我们不能再在主线程中对它调用 drop。相反,我们会收到这个编译器错误:

$ cargo run
   Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
  --> src/main.rs:10:10
   |
4  |     let v = vec![1, 2, 3];
   |         - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5  |
6  |     let handle = thread::spawn(move || {
   |                                ------- value moved into closure here
7  |         println!("Here's a vector: {v:?}");
   |                                     - variable moved due to use in closure
...
10 |     drop(v); // oh no!
   |          ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error


Rust 的所有权规则再次拯救了我们!我们从示例 16-3 中的代码中得到了一个错误,因为 Rust 很保守,只为线程借用了 v,这意味着主线程理论上可以使生成的线程的引用无效。通过告诉 Rust 将 v 的所有权移动到生成的线程,我们向 Rust 保证主线程不会再使用 v。如果我们以同样的方式更改示例 16-4,那么当我们尝试在主线程中使用 v 时,我们就违反了所有权规则。move 关键字覆盖了 Rust 的保守默认 borrowing;它不允许我们违反所有权规则。


对线程和线程 API 有了基本的了解后,让我们看看我们可以用线程做什么