使用线程同时运行代码

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

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

  • 争用条件,其中线程访问 顺序不一致
  • 死锁,其中两个线程相互等待,从而阻止两个线程 线程继续
  • 仅在某些情况下发生且难以重现和修复的错误 可靠地

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

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

使用 创建新线程spawn

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

文件名: 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强制线程暂时停止执行 duration,允许其他线程运行。线程可能需要 转动,但这并不能保证:这取决于您的作系统如何 调度线程。在此运行中,主线程首先打印,即使 spawned thread 的 print 语句首先出现在代码中。甚至 尽管我们告诉生成的线程打印直到i是 9,则只到了 5 在主线程关闭之前。

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

等待所有线程完成使用join处理

示例 16-1 中的代码不仅大部分时间过早地停止了生成的线程 时间由于主线程结束,但因为不能保证 线程运行的顺序,我们也不能保证生成的线程 根本就要跑了!

我们可以修复 spawned thread 无法运行或提前结束的问题 通过保存thread::spawn在变量中。返回类型thread::spawnJoinHandle.一个JoinHandle是一个拥有的值,当我们 调用join方法,将等待其线程完成。示例 16-2 演示如何使用JoinHandle我们在示例 16-1 中创建的线程中,并且 叫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:保存JoinHandlethread::spawn保证线程运行完成

join在手柄上阻塞当前正在运行的线程,直到 handle 表示的 thread 终止。阻塞线程意味着 thread 无法执行工作或退出。因为我们已经发出了决定 自join在主线程的for循环,运行示例 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循环输入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));
        }
    });

    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在 Closure 的上下文中。现在 我们将更多地关注movethread::spawn.

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

文件名: 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:尝试使用由 主线程在另一个线程中

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

$ 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,则 Closure 会尝试借用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 允许我们运行此代码,则有可能生成线程 将立即置于后台,根本不运行。生成的 thread 引用了v内部,但主线程立即下降v,使用drop函数。然后,当 spawned thread 开始执行,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关键字,我们强制 Closure 采用 它正在使用的值的所有权,而不是允许 Rust 推断它 应该借用这些值。清单 中所示的对示例 16-3 的修改 16-5 将按照我们的预期编译和运行:

文件名: 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:使用movekeyword 强制闭包 获得它使用的值的所有权

我们可能很想尝试同样的事情来修复示例 16-4 中的代码,其中 名为drop通过使用move关闭。但是,此修复程序将 不起作用,因为示例 16-4 试图做的事情对于 不同的原因。如果我们添加了move到 Closure 时,我们会移动v到 closure 的环境,我们不能再调用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对于 thread 的 thread 的引用。通过告诉 Rust 移动v到生成的 thread 的 Rust 中,我们保证主线程不会使用v了。如果 我们以同样的方式更改示例 16-4,然后我们违反了所有权 规则v在主线程中。这move关键字覆盖 Rust 保守的借款违约;它不允许我们违反 所有权规则。

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

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准