|
|
|
@ -1,10 +1,9 @@
|
|
|
|
|
## 使用线程同时运行代码
|
|
|
|
|
|
|
|
|
|
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md)
|
|
|
|
|
> <br>
|
|
|
|
|
> commit 8aecae3efe5ca8f79f055b70f05d9a3f990bce7b
|
|
|
|
|
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md -->
|
|
|
|
|
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
|
|
|
|
|
|
|
|
|
|
在大部分现代操作系统中,已执行程序的代码在一个 **进程**(_process_)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 **线程**(_threads_)。例如,web 服务器可以有多个线程以便可以同时响应多个请求。
|
|
|
|
|
在大部分现代操作系统中,已执行程序的代码在一个**进程**(_process_)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为**线程**(_threads_)。例如,web 服务端可以有多个线程以便可以同时响应多个请求。
|
|
|
|
|
|
|
|
|
|
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
|
|
|
|
|
|
|
|
|
@ -14,7 +13,7 @@
|
|
|
|
|
|
|
|
|
|
Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。
|
|
|
|
|
|
|
|
|
|
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 *1:1* 线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。
|
|
|
|
|
编程语言实现线程的方式各不相同,许多操作系统都提供了供语言调用以创建新线程的 API。Rust 标准库使用 *1:1* 模型的线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。(Rust 的 async 系统,我们将在下一章看到,也提供了另一种并发方式。)
|
|
|
|
|
|
|
|
|
|
### 使用 `spawn` 创建新线程
|
|
|
|
|
|
|
|
|
@ -26,9 +25,9 @@ Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编
|
|
|
|
|
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-01/src/main.rs}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容</span>
|
|
|
|
|
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程同时主线程打印其它内容</span>
|
|
|
|
|
|
|
|
|
|
注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
|
|
|
|
|
注意当 Rust 程序的主线程结束时,所有新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
hi number 1 from the main thread!
|
|
|
|
@ -50,7 +49,7 @@ hi number 5 from the spawned thread!
|
|
|
|
|
|
|
|
|
|
由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,我们甚至不能实际保证新建线程会被执行!
|
|
|
|
|
|
|
|
|
|
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle`。`JoinHandle` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行:
|
|
|
|
|
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle<T>`。`JoinHandle<T>` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle<T>` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行。
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
@ -58,9 +57,9 @@ hi number 5 from the spawned thread!
|
|
|
|
|
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-02/src/main.rs}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle` 以确保该线程能够运行至结束</span>
|
|
|
|
|
<span class="caption">示例 16-2: 从 `thread::spawn` 保存一个 `JoinHandle<T>` 以确保该线程能够运行至结束</span>
|
|
|
|
|
|
|
|
|
|
通过调用 handle 的 `join` 会阻塞当前线程直到 handle 所代表的线程结束。**阻塞**(_Blocking_)线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出:
|
|
|
|
|
通过调用句柄的 `join` 会阻塞当前线程直到句柄所代表的线程结束。**阻塞**(_Blocking_)线程意味着阻止该线程执行工作或退出。因为我们将 `join` 调用放在了主线程的 `for` 循环之后,运行示例 16-2 应该会产生类似这样的输出:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
hi number 1 from the main thread!
|
|
|
|
@ -78,7 +77,7 @@ hi number 8 from the spawned thread!
|
|
|
|
|
hi number 9 from the spawned thread!
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用会等待直到新建线程执行完毕。
|
|
|
|
|
这两个线程仍然会交替执行,不过主线程会由于 `handle.join()` 调用而不会结束直到新建线程执行完毕。
|
|
|
|
|
|
|
|
|
|
不过让我们看看将 `handle.join()` 移动到 `main` 中 `for` 循环之前会发生什么,如下:
|
|
|
|
|
|
|
|
|
@ -110,7 +109,7 @@ hi number 4 from the main thread!
|
|
|
|
|
|
|
|
|
|
### 将 `move` 闭包与线程一同使用
|
|
|
|
|
|
|
|
|
|
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章 [“使用闭包捕获环境”][capture] 部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move` 和 `thread::spawn` 之间的交互。
|
|
|
|
|
`move` 关键字经常用于传递给 `thread::spawn` 的闭包,因为闭包会获取从环境中取得的值的所有权,因此会将这些值的所有权从一个线程传送到另一个线程。在第十三章[“使用闭包捕获环境”][capture]部分讨论了闭包上下文中的 `move`。现在我们会更专注于 `move` 和 `thread::spawn` 之间的交互。
|
|
|
|
|
|
|
|
|
|
在第十三章中,我们讲到可以在参数列表前使用 `move` 关键字强制闭包获取其使用的环境值的所有权。这个技巧在创建新线程将值的所有权从一个线程移动到另一个线程时最为实用。
|
|
|
|
|
|
|
|
|
@ -130,7 +129,7 @@ hi number 4 from the main thread!
|
|
|
|
|
{{#include ../listings/ch16-fearless-concurrency/listing-16-03/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Rust 会 **推断** 如何捕获 `v`,因为 `println!` 只需要 `v` 的引用,闭包尝试借用 `v`。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓对 `v` 的引用是否一直有效。
|
|
|
|
|
Rust 会**推断**如何捕获 `v`,因为 `println!` 只需要 `v` 的引用,闭包尝试借用 `v`。然而这有一个问题:Rust 不知道这个新建线程会执行多久,所以无法知晓对 `v` 的引用是否一直有效。
|
|
|
|
|
|
|
|
|
|
示例 16-4 展示了一个 `v` 的引用很有可能不再有效的场景:
|
|
|
|
|
|
|
|
|
@ -153,7 +152,7 @@ help: to force the closure to take ownership of `v` (and any other referenced va
|
|
|
|
|
| ++++
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
通过在闭包之前增加 `move` 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。示例 16-5 中展示的对示例 16-3 代码的修改,可以按照我们的预期编译并运行:
|
|
|
|
|
通过在闭包之前增加 `move` 关键字,我们强制闭包获取其使用的值的所有权,而不是任由 Rust 推断它应该借用值。示例 16-5 中展示的对示例 16-3 代码的修改,就能按预期编译并运行:
|
|
|
|
|
|
|
|
|
|
<span class="filename">文件名:src/main.rs</span>
|
|
|
|
|
|
|
|
|
@ -169,8 +168,8 @@ help: to force the closure to take ownership of `v` (and any other referenced va
|
|
|
|
|
{{#include ../listings/ch16-fearless-concurrency/output-only-01-move-drop/output.txt}}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因为 Rust 是保守的并只会为线程借用 `v`,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 `v` 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 `v`。如果对示例 16-4 也做出如此修改,那么当在主线程中使用 `v` 时就会违反所有权规则。 `move` 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。
|
|
|
|
|
Rust 的所有权规则又一次帮助了我们!示例 16-3 中的错误是因为 Rust 是保守的并只会为线程借用 `v`,这意味着主线程理论上可能使新建线程的引用无效。通过告诉 Rust 将 `v` 的所有权移动到新建线程,我们向 Rust 保证主线程不会再使用 `v`。如果对示例 16-4 也做出如此修改,那么当在主线程中使用 `v` 时就会违反所有权规则。`move` 关键字覆盖了 Rust 默认保守的借用,但它不允许我们违反所有权规则。
|
|
|
|
|
|
|
|
|
|
现在我们对线程和线程 API 有了基本的了解,让我们讨论一下使用线程实际可以 **做** 什么吧。
|
|
|
|
|
现在我们已经了解了线程的概念以及线程 API 提供的方法,下面让我们看看在什么情况下可以使用线程。
|
|
|
|
|
|
|
|
|
|
[capture]: ch13-01-closures.html#使用闭包捕获环境
|
|
|
|
|