mirror of
https://github.com/KaiserY/trpl-zh-cn
synced 2024-11-09 08:51:18 +08:00
update ch16
This commit is contained in:
parent
4c46c1da30
commit
6a952961bd
@ -1,6 +1,7 @@
|
||||
# 无畏并发
|
||||
|
||||
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md) <br>
|
||||
> [ch16-00-concurrency.md](https://github.com/rust-lang/book/blob/main/src/ch16-00-concurrency.md)
|
||||
> <br>
|
||||
> commit 1fedfc4b96c2017f64ecfcf41a0a07e2e815f24f
|
||||
|
||||
安全且高效的处理并发编程是 Rust 的另一个主要目标。**并发编程**(_Concurrent programming_),代表程序的不同部分相互独立的执行,而 **并行编程**(_parallel programming_)代表程序不同部分于同时执行,这两个概念随着计算机越来越多的利用多处理器的优势时显得愈发重要。由于历史原因,在此类上下文中编程一直是困难且容易出错的:Rust 希望能改变这一点。
|
||||
|
@ -1,19 +1,20 @@
|
||||
## 使用线程同时运行代码
|
||||
|
||||
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md) <br>
|
||||
> commit 6b9eae8ce91dd0d94982795762d22077d372e90c
|
||||
> [ch16-01-threads.md](https://github.com/rust-lang/book/blob/main/src/ch16-01-threads.md)
|
||||
> <br>
|
||||
> commit 8aecae3efe5ca8f79f055b70f05d9a3f990bce7b
|
||||
|
||||
在大部分现代操作系统中,已执行程序的代码在一个 **进程**(_process_)中运行,操作系统则负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。运行这些独立部分的功能被称为 **线程**(_threads_)。
|
||||
在大部分现代操作系统中,已执行程序的代码在一个 **进程**(_process_)中运行,操作系统则会负责管理多个进程。在程序内部,也可以拥有多个同时运行的独立部分。这些运行这些独立部分的功能被称为 **线程**(_threads_)。例如,web 服务器可以有多个线程以便可以同时响应多余一个请求。
|
||||
|
||||
将程序中的计算拆分进多个线程可以改善性能,因为程序可以同时进行多个任务,不过这也会增加复杂性。因为线程是同时运行的,所以无法预先保证不同线程中的代码的执行顺序。这会导致诸如此类的问题:
|
||||
|
||||
- 竞态条件(Race conditions),多个线程以不一致的顺序访问数据或资源
|
||||
- 死锁(Deadlocks),两个线程相互等待对方停止使用其所拥有的资源,这会阻止它们继续运行
|
||||
- 死锁(Deadlocks),两个线程相互等待对方,这会阻止两者继续运行
|
||||
- 只会发生在特定情况且难以稳定重现和修复的 bug
|
||||
|
||||
Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编程仍需格外小心,同时其所要求的代码结构也不同于运行于单线程的程序。
|
||||
|
||||
编程语言有一些不同的方法来实现线程。很多操作系统提供了创建新线程的 API。这种由编程语言调用操作系统 API 创建线程的模型有时被称为 *1:1*,一个 OS 线程对应一个语言线程。Rust 标准库只提供了 1:1 线程实现;有一些 crate 实现了其他有着不同取舍的线程模型。
|
||||
编程语言有一些不同的方法来实现线程,而且很多操作系统提供了创建新线程的 API。Rust 标准库使用 *1:1* 线程实现,这代表程序的每一个语言级线程使用一个系统线程。有一些 crate 实现了其他有着不同于 1:1 模型取舍的线程模型。
|
||||
|
||||
### 使用 `spawn` 创建新线程
|
||||
|
||||
@ -27,7 +28,7 @@ Rust 尝试减轻使用线程的负面影响。不过在多线程上下文中编
|
||||
|
||||
<span class="caption">示例 16-1: 创建一个打印某些内容的新线程,但是主线程打印其它内容</span>
|
||||
|
||||
注意这个函数编写的方式,当主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
|
||||
注意当 Rust 程序的主线程结束时,新线程也会结束,而不管其是否执行完毕。这个程序的输出可能每次都略有不同,不过它大体上看起来像这样:
|
||||
|
||||
```text
|
||||
hi number 1 from the main thread!
|
||||
@ -47,7 +48,7 @@ hi number 5 from the spawned thread!
|
||||
|
||||
#### 使用 `join` 等待所有线程结束
|
||||
|
||||
由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,甚至不能实际保证新建线程会被执行。其原因在于无法保证线程运行的顺序!
|
||||
由于主线程结束,示例 16-1 中的代码大部分时候不光会提早结束新建线程,因为无法保证线程运行的顺序,我们甚至不能实际保证新建线程会被执行!
|
||||
|
||||
可以通过将 `thread::spawn` 的返回值储存在变量中来修复新建线程部分没有执行或者完全没有执行的问题。`thread::spawn` 的返回值类型是 `JoinHandle`。`JoinHandle` 是一个拥有所有权的值,当对其调用 `join` 方法时,它会等待其线程结束。示例 16-2 展示了如何使用示例 16-1 中创建的线程的 `JoinHandle` 并调用 `join` 来确保新建线程在 `main` 退出前结束运行:
|
||||
|
||||
@ -141,7 +142,7 @@ Rust 会 **推断** 如何捕获 `v`,因为 `println!` 只需要 `v` 的引用
|
||||
|
||||
<span class="caption">示例 16-4: 一个具有闭包的线程,尝试使用一个在主线程中被回收的引用 `v`</span>
|
||||
|
||||
假如这段代码能正常运行的话,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第十五章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了!
|
||||
如果 Rust 允许这段代码运行,则新建线程则可能会立刻被转移到后台并完全没有机会运行。新建线程内部有一个 `v` 的引用,不过主线程立刻就使用第十五章讨论的 `drop` 丢弃了 `v`。接着当新建线程开始执行,`v` 已不再有效,所以其引用也是无效的。噢,这太糟了!
|
||||
|
||||
为了修复示例 16-3 的编译错误,我们可以听取错误信息的建议:
|
||||
|
||||
@ -162,7 +163,7 @@ help: to force the closure to take ownership of `v` (and any other referenced va
|
||||
|
||||
<span class="caption">示例 16-5: 使用 `move` 关键字强制获取它使用的值的所有权</span>
|
||||
|
||||
那么如果使用了 `move` 闭包,示例 16-4 中主线程调用了 `drop` 的代码会发生什么呢?加了 `move` 就搞定了吗?不幸的是,我们会得到一个不同的错误,因为示例 16-4 所尝试的操作由于一个不同的原因而不被允许。如果为闭包增加 `move`,将会把 `v` 移动进闭包的环境中,如此将不能在主线程中对其调用 `drop` 了。我们会得到如下不同的编译错误:
|
||||
我们可能希望尝试同样的方法来修复示例 16-4 中的代码,其主线程使用 `move` 闭包调用了 `drop`。然而这个修复行不通,因为示例 16-4 所尝试的操作由于一个不同的原因而不被允许。如果为闭包增加 `move`,将会把 `v` 移动进闭包的环境中,如此将不能在主线程中对其调用 `drop` 了。我们会得到如下不同的编译错误:
|
||||
|
||||
```console
|
||||
{{#include ../listings/ch16-fearless-concurrency/output-only-01-move-drop/output.txt}}
|
||||
|
@ -1,15 +1,18 @@
|
||||
## 使用消息传递在线程间传送数据
|
||||
|
||||
> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/main/src/ch16-02-message-passing.md) <br>
|
||||
> commit 24e275d624fe85af7b5b6316e78f8bfbbcac23e7
|
||||
> [ch16-02-message-passing.md](https://github.com/rust-lang/book/blob/main/src/ch16-02-message-passing.md)
|
||||
> <br>
|
||||
> commit 36383b4da21dbd0a0781473bc8ad7ef0ed1b6751
|
||||
|
||||
一个日益流行的确保安全并发的方式是 **消息传递**(_message passing_),这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号:“不要通过共享内存来通讯;而是通过通讯来共享内存。”(“Do not communicate by sharing memory; instead, share memory by communicating.”)
|
||||
|
||||
Rust 中一个实现消息传递并发的主要工具是 **信道**(_channel_),Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。
|
||||
为了实现消息传递并发,Rust 标准库提供了一个 **信道**(_channel_)实现。信道是一个通用编程概念,表示数据从一个线程发送到另一个线程。
|
||||
|
||||
你可以将编程中的信道想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。
|
||||
|
||||
编程中的信息渠道(信道)有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。发送者位于上游位置,在这里可以将橡皮鸭放入河中,接收者则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为信道被 **关闭**(_closed_)了。
|
||||
|
||||
这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用信道来实现聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
|
||||
这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,你就可以将信道用于任何相互通信的任何线程,例如一个聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
|
||||
|
||||
首先,在示例 16-6 中,创建了一个信道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在信道中发送什么类型:
|
||||
|
||||
@ -23,7 +26,7 @@ Rust 中一个实现消息传递并发的主要工具是 **信道**(_channel_
|
||||
|
||||
这里使用 `mpsc::channel` 函数创建一个新的信道;`mpsc` 是 **多个生产者,单个消费者**(_multiple producer, single consumer_)的缩写。简而言之,Rust 标准库实现信道的方式意味着一个信道可以有多个产生值的 **发送**(_sending_)端,但只能有一个消费这些值的 **接收**(_receiving_)端。想象一下多条小河小溪最终汇聚成大河:所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始,但是当示例可以工作后会增加多个生产者。
|
||||
|
||||
`mpsc::channel` 函数返回一个元组:第一个元素是发送端,而第二个元素是接收端。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(_transmitter_)和 **接收者**(_receiver_)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十八章会讨论 `let` 语句中的模式和解构。如此使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。
|
||||
`mpsc::channel` 函数返回一个元组:第一个元素是发送端 -- 发送者,而第二个元素是接收端 -- 接收者。由于历史原因,`tx` 和 `rx` 通常作为 **发送者**(_transmitter_)和 **接收者**(_receiver_)的缩写,所以这就是我们将用来绑定这两端变量的名字。这里使用了一个 `let` 语句和模式来解构了此元组;第十八章会讨论 `let` 语句中的模式和解构。现在只需知道使用 `let` 语句是一个方便提取 `mpsc::channel` 返回的元组中一部分的手段。
|
||||
|
||||
让我们将发送端移动到一个新建线程中并发送一个字符串,这样新建线程就可以和主线程通讯了,如示例 16-7 所示。这类似于在河的上游扔下一只橡皮鸭或从一个线程向另一个线程发送聊天信息:
|
||||
|
||||
@ -35,11 +38,9 @@ Rust 中一个实现消息传递并发的主要工具是 **信道**(_channel_
|
||||
|
||||
<span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi”</span>
|
||||
|
||||
这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。
|
||||
这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result<T, E>` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。
|
||||
|
||||
信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result<T, E>` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。
|
||||
|
||||
在示例 16-8 中,我们在主线程中从信道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息:
|
||||
在示例 16-8 中,我们在主线程中从信道的接收者获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -49,7 +50,7 @@ Rust 中一个实现消息传递并发的主要工具是 **信道**(_channel_
|
||||
|
||||
<span class="caption">示例 16-8: 在主线程中接收并打印内容 “hi”</span>
|
||||
|
||||
信道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result<T, E>` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。
|
||||
信道的接收者有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result<T, E>` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。
|
||||
|
||||
`try_recv` 不会阻塞,相反它立刻返回一个 `Result<T, E>`:`Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
|
||||
|
||||
@ -112,7 +113,7 @@ Got: thread
|
||||
|
||||
### 通过克隆发送者来创建多个生产者
|
||||
|
||||
之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建向同一接收者发送值的多个线程。这可以通过克隆信道的发送端来做到,如示例 16-11 所示:
|
||||
之前我们提到了`mpsc`是 _multiple producer, single consumer_ 的缩写。可以运用 `mpsc` 来扩展示例 16-10 中的代码来创建向同一接收者发送值的多个线程。这可以通过克隆发送者来做到,如示例 16-11 所示:
|
||||
|
||||
<span class="filename">文件名:src/main.rs</span>
|
||||
|
||||
@ -122,7 +123,7 @@ Got: thread
|
||||
|
||||
<span class="caption">示例 16-11: 从多个生产者发送多个消息</span>
|
||||
|
||||
这一次,在创建新线程之前,我们对信道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
|
||||
这一次,在创建新线程之前,我们对发送者调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
|
||||
|
||||
如果运行这些代码,你 **可能** 会看到这样的输出:
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
## 共享状态并发
|
||||
|
||||
> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md) <br>
|
||||
> commit 75b9d4a8dccc245e0343eb1480aa86f169043ea5
|
||||
> [ch16-03-shared-state.md](https://github.com/rust-lang/book/blob/main/src/ch16-03-shared-state.md)
|
||||
> <br>
|
||||
> commit 856d89c53a6d69470bb5669c773fdfe6aab6fcc9
|
||||
|
||||
虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。再一次思考一下 Go 编程语言文档中口号的这一部分:“不要通过共享内存来通讯”(“do not communicate by sharing memory.”):
|
||||
虽然消息传递是一个很好的处理并发的方式,但并不是唯一一个。另一种方式是让多个线程拥有相同的共享数据。再一次思考一下 Go 编程语言文档中口号的这一部分:“不要通过共享内存来通讯”(“do not communicate by sharing memory.”):
|
||||
|
||||
> What would communicating by sharing memory look like? In addition, why would message passing enthusiasts not use it and do the opposite instead?
|
||||
> What would communicating by sharing memory look like? In addition, why would message-passing enthusiasts caution not to use memory sharing?
|
||||
>
|
||||
> 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢?
|
||||
> 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者对共享内存如此谨慎呢?
|
||||
|
||||
在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。
|
||||
|
||||
@ -40,7 +41,7 @@
|
||||
|
||||
如果另一个线程拥有锁,并且那个线程 panic 了,则 `lock` 调用会失败。在这种情况下,没人能够再获取锁,所以这里选择 `unwrap` 并在遇到这种情况时使线程 panic。
|
||||
|
||||
一旦获取了锁,就可以将返回值(在这里是`num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁:`Mutex<i32>` 并不是一个 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 `i32` 值。
|
||||
一旦获取了锁,就可以将返回值(在这里是`num`)视为一个其内部数据的可变引用了。类型系统确保了我们在使用 `m` 中的值之前获取锁。`m` 的类型是 `Mutex<i32>` 而不是 `i32`,所以 **必须** 获取锁才能使用这个 `i32` 值。我们是不会忘记这么做的,因为反之类型系统不允许访问内部的 `i32` 值。
|
||||
|
||||
正如你所怀疑的,`Mutex<T>` 是一个智能指针。更准确的说,`lock` 调用 **返回** 一个叫做 `MutexGuard` 的智能指针。这个智能指针实现了 `Deref` 来指向其内部数据;其也提供了一个 `Drop` 实现当 `MutexGuard` 离开作用域时自动释放锁,这正发生于示例 16-12 内部作用域的结尾。为此,我们不会忘记释放锁并阻塞互斥器为其它线程所用的风险,因为锁的释放是自动发生的。
|
||||
|
||||
@ -116,6 +117,8 @@ Result: 10
|
||||
|
||||
成功了!我们从 0 数到了 10,这可能并不是很显眼,不过一路上我们确实学习了很多关于 `Mutex<T>` 和线程安全的内容!这个例子中构建的结构可以用于比增加计数更为复杂的操作。使用这个策略,可将计算分成独立的部分,分散到多个线程中,接着使用 `Mutex<T>` 使用各自的结算结果更新最终的结果。
|
||||
|
||||
注意如果是简单的数值运算,[标准库中 `std::sync::atomic` 模块][atomic] 提供的比 `Mutex<T>` 更简单的类型。这些类型提供了基本类型之上安全、并发、原子的操作。这个例子中选择在基本类型上使用 `Mutex<T>` 以便我们可以专注于 `Mutex<T>` 如何工作。
|
||||
|
||||
### `RefCell<T>`/`Rc<T>` 与 `Mutex<T>`/`Arc<T>` 的相似性
|
||||
|
||||
你可能注意到了,因为 `counter` 是不可变的,不过可以获取其内部值的可变引用;这意味着 `Mutex<T>` 提供了内部可变性,就像 `Cell` 系列类型那样。正如第十五章中使用 `RefCell<T>` 可以改变 `Rc<T>` 中的内容那样,同样的可以使用 `Mutex<T>` 来改变 `Arc<T>` 中的内容。
|
||||
|
@ -1,7 +1,8 @@
|
||||
## 使用 `Sync` 和 `Send` trait 的可扩展并发
|
||||
|
||||
> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/main/src/ch16-04-extensible-concurrency-sync-and-send.md) <br>
|
||||
> commit a7a6804a2444ee05ff8b93f54973a9ce0f6511c1
|
||||
> [ch16-04-extensible-concurrency-sync-and-send.md](https://github.com/rust-lang/book/blob/main/src/ch16-04-extensible-concurrency-sync-and-send.md)
|
||||
> <br>
|
||||
> commit 7c7740a5ddef1458d74f1daf85fd49e03aaa97cf
|
||||
|
||||
Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **甚少**。我们之前讨论的几乎所有内容,都属于标准库,而不是语言本身的内容。由于不需要语言提供并发相关的基础设施,并发方案不受标准库或语言所限:我们可以编写自己的或使用别人编写的并发功能。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user