wip: 2024 edition

pull/875/head
KaiserY 2 weeks ago
parent 82e4e9b08c
commit d4266d4fb5

@ -1,20 +1,19 @@
# 无畏并发
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md)
> <br>
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
<!-- https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md -->
<!-- commit 56ec353290429e6547109e88afea4de027b0f1a9 -->
安全且高效地处理并发编程是 Rust 的另一个主要目标。**并发编程**_Concurrent programming_代表程序的不同部分相互独立地执行 **并行编程**_parallel programming_代表程序不同部分同时执行这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因在此类上下文中编程一直是困难且容易出错的Rust 希望能改变这一
安全且高效地处理并发编程是 Rust 的另一个主要目标。**并发编程**_Concurrent programming_代表程序的不同部分相互独立地执行而**并行编程**_parallel programming_代表程序不同部分同时执行这两个概念随着计算机越来越多的利用多处理器的优势而显得愈发重要。由于历史原因在此类上下文中编程一直是困难且容易出错的Rust 希望能改变这一现状
起初Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全 **和** 并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是 **编译时** 错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,Rust 会拒绝编译不正确的代码并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号 **无畏并发**_fearless concurrency_。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。
起初Rust 团队认为确保内存安全和防止并发问题是两个分别需要不同方法应对的挑战。随着时间的推移,团队发现所有权和类型系统是一系列解决内存安全**和**并发问题的强有力的工具!通过利用所有权和类型检查,在 Rust 中很多并发错误都是**编译时**错误,而非运行时错误。因此,相比花费大量时间尝试重现运行时并发 bug 出现的特定情况,不正确的代码会直接编译失败并提供解释问题的错误信息。因此,你可以在开发时修复代码,而不是在部署到生产环境后修复代码。我们给 Rust 的这一部分起了一个绰号**无畏并发**_fearless concurrency_。无畏并发令你的代码免于出现诡异的 bug 并可以轻松重构且无需担心会引入新的 bug。
> 注意:出于简洁的考虑,我们将很多问题归类为 **并发**,而不是更准确的区分 **并发和(或)并行**。如果这是一本专注于并发和/或并行的书,我们肯定会更加精确的。对于本章,当我们谈到 **并发** 时,请自行脑内替换为 **并发和(或)并行**
> 注意:出于简洁的考虑,我们将很多问题归类为**并发**,而不是更准确的区分**并发和和/或并行**。对于本章,当我们谈到**并发**时,请自行脑内替换为 **并发和和/或并行**。在下一章中当区分二者更为重要时,我们会使用更准确的表述
很多语言所提供的处理并发问题的解决方法都非常有特色。例如Erlang 有着优雅的消息传递并发功能但只有模糊不清的在线程间共享状态的方法。对于高级语言来说只实现可能解决方案的子集是一个合理的策略因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。
很多语言所提供的处理并发问题的解决方法都非常有。例如Erlang 有着优雅的消息传递message-passing并发功能但只有模糊不清的在线程间共享状态的方法。对于高级语言来说只实现可能解决方案的子集是一个合理的策略因为高级语言所许诺的价值来源于牺牲一些控制来换取抽象。然而对于底层语言则期望提供在任何给定的情况下有着最高的性能且对硬件有更少的抽象。因此Rust 提供了多种工具,以符合实际情况和需求的方式来为问题建模。
如下是本章将要涉及到的内容:
* 如何创建线程来同时运行多段代码。
* **消息传递**_Message passing_并发其中信道channel被用来在线程间传递消息。
* **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。
* `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。
- 如何创建线程来同时运行多段代码。
- **消息传递**_Message passing_并发其中信道channel被用来在线程间传递消息。
- **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。
- `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。

@ -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#使用闭包捕获环境

Loading…
Cancel
Save