更改channel的翻译

pull/599/head
juicyenc 3 years ago committed by GitHub
parent 123a06eecb
commit 3c4a7a5ee5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,6 +14,6 @@
如下是本章将要涉及到的内容: 如下是本章将要涉及到的内容:
* 如何创建线程来同时运行多段代码。 * 如何创建线程来同时运行多段代码。
* **消息传递**_Message passing_并发其中channel被用来在线程间传递消息。 * **消息传递**_Message passing_并发其中channel被用来在线程间传递消息。
* **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。 * **共享状态**_Shared state_并发其中多个线程可以访问同一片数据。
* `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。 * `Sync``Send` trait将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。

@ -5,13 +5,13 @@
一个日益流行的确保安全并发的方式是 **消息传递**_message passing_这里线程或 actor 通过发送包含数据的消息来相互沟通。这个思想来源于 [Go 编程语言文档中](https://golang.org/doc/effective_go.html#concurrency) 的口号“不要通过共享内存来通讯而是通过通讯来共享内存。”“Do not communicate by sharing memory; instead, share memory by communicating.”) 一个日益流行的确保安全并发的方式是 **消息传递**_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_Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。
编程中的通道有两部分组成一个发送者transmitter和一个接收者receiver。发送者位于上游位置在这里可以将橡皮鸭放入河中接收者则位于下游橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为道被 **关闭**_closed_了。 编程中的信息渠道(信道)有两部分组成一个发送者transmitter和一个接收者receiver。发送者位于上游位置在这里可以将橡皮鸭放入河中接收者则位于下游橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为道被 **关闭**_closed_了。
这里,我们将开发一个程序,它会在一个线程生成值向通道发送,而在另一个线程会接收值并打印出来。这里会通过通道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用通道来实现聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。 这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用信道来实现聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。
首先,在示例 16-6 中,创建了一个通道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在通道中发送什么类型: 首先,在示例 16-6 中,创建了一个信道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在信道中发送什么类型:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -19,9 +19,9 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**_channel_
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-06/src/main.rs}} {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-06/src/main.rs}}
``` ```
<span class="caption">示例 16-6: 创建一个道,并将其两端赋值给 `tx``rx`</span> <span class="caption">示例 16-6: 创建一个道,并将其两端赋值给 `tx``rx`</span>
这里使用 `mpsc::channel` 函数创建一个新的道;`mpsc` 是 **多个生产者,单个消费者**_multiple producer, single consumer_的缩写。简而言之Rust 标准库实现通道的方式意味着一个通道可以有多个产生值的 **发送**_sending_但只能有一个消费这些值的 **接收**_receiving_端。想象一下多条小河小溪最终汇聚成大河所有通过这些小河发出的东西最后都会来到下游的大河。目前我们以单个生产者开始但是当示例可以工作后会增加多个生产者。 这里使用 `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` 返回的元组中一部分的手段。
@ -35,11 +35,11 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**_channel_
<span class="caption">示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi”</span> <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> <span class="filename">文件名: src/main.rs</span>
@ -49,7 +49,7 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**_channel_
<span class="caption">示例 16-8: 在主线程中接收并打印内容 “hi”</span> <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`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。 `try_recv` 不会阻塞,相反它立刻返回一个 `Result<T, E>``Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。
@ -63,9 +63,9 @@ Got: hi
完美! 完美!
### 道与所有权转移 ### 道与所有权转移
所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个试验来看看通道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的通道中发送完 `val`**之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: 所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个试验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val`**之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的:
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -73,9 +73,9 @@ Got: hi
{{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-09/src/main.rs}} {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-09/src/main.rs}}
``` ```
<span class="caption">示例 16-9: 在我们已经发送到道中后,尝试使用 `val` 引用</span> <span class="caption">示例 16-9: 在我们已经发送到道中后,尝试使用 `val` 引用</span>
这里尝试在通过 `tx.send` 发送 `val`道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时Rust 会给出一个错误: 这里尝试在通过 `tx.send` 发送 `val`道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时Rust 会给出一个错误:
```console ```console
{{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}} {{#include ../listings/ch16-fearless-concurrency/listing-16-09/output.txt}}
@ -85,7 +85,7 @@ Got: hi
### 发送多个值并观察接收者的等待 ### 发送多个值并观察接收者的等待
示例 16-8 中的代码可以编译和运行,不过它并没有明确的告诉我们两个独立的线程通过道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。 示例 16-8 中的代码可以编译和运行,不过它并没有明确的告诉我们两个独立的线程通过道相互通讯。示例 16-10 则有一些改进会证明示例 16-8 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。
<span class="filename">文件名: src/main.rs</span> <span class="filename">文件名: src/main.rs</span>
@ -97,7 +97,7 @@ Got: hi
这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。 这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。
在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当道被关闭时,迭代器也将结束。 在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当道被关闭时,迭代器也将结束。
当运行示例 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒: 当运行示例 16-10 中的代码时,将看到如下输出,每一行都会暂停一秒:
@ -112,7 +112,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> <span class="filename">文件名: src/main.rs</span>
@ -122,7 +122,7 @@ Got: thread
<span class="caption">示例 16-11: 从多个生产者发送多个消息</span> <span class="caption">示例 16-11: 从多个生产者发送多个消息</span>
这一次,在创建新线程之前,我们对道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向通道的接收端发送不同的消息。 这一次,在创建新线程之前,我们对道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。
如果运行这些代码,你 **可能** 会看到这样的输出: 如果运行这些代码,你 **可能** 会看到这样的输出:
@ -139,4 +139,4 @@ Got: you
虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定,且每次都会产生不同的输出。 虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定,且每次都会产生不同的输出。
现在我们见识过了道如何工作,再看看另一种不同的并发方式吧。 现在我们见识过了道如何工作,再看看另一种不同的并发方式吧。

@ -9,7 +9,7 @@
> >
> 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢? > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢?
在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中将无法再使用这个值。共享内存类似于多所有权多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能然而这会增加额外的复杂性因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中将无法再使用这个值。共享内存类似于多所有权多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能然而这会增加额外的复杂性因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。
### 互斥器一次只允许一个线程访问数据 ### 互斥器一次只允许一个线程访问数据
@ -22,7 +22,7 @@
作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行! 作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行!
正确的管理互斥器异常复杂,这也是许多人之所以热衷于道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。 正确的管理互斥器异常复杂,这也是许多人之所以热衷于道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。
### `Mutex<T>`的 API ### `Mutex<T>`的 API

@ -33,7 +33,7 @@ Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 **
正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。
Rust 提供了用于消息传递的道,和像 `Mutex<T>``Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念无所畏惧地并发吧 Rust 提供了用于消息传递的道,和像 `Mutex<T>``Arc<T>` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念无所畏惧地并发吧
接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程Object Oriented Programming中那些你所熟悉的概念相联系的。 接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程Object Oriented Programming中那些你所熟悉的概念相联系的。

@ -233,21 +233,21 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
这段代码能够编译并用指定给 `ThreadPool::new` 的参数创建储存了一系列的 `Worker` 实例,不过 **仍然** 没有处理 `execute` 中得到的闭包。让我们聊聊接下来怎么做。 这段代码能够编译并用指定给 `ThreadPool::new` 的参数创建储存了一系列的 `Worker` 实例,不过 **仍然** 没有处理 `execute` 中得到的闭包。让我们聊聊接下来怎么做。
#### 使用道向线程发送请求 #### 使用道向线程发送请求
下一个需要解决的问题是传递给 `thread::spawn` 的闭包完全没有做任何工作。目前,我们在 `execute` 方法中获得期望执行的闭包,不过在创建 `ThreadPool` 的过程中创建每一个 `Worker` 时需要向 `thread::spawn` 传递一个闭包。 下一个需要解决的问题是传递给 `thread::spawn` 的闭包完全没有做任何工作。目前,我们在 `execute` 方法中获得期望执行的闭包,不过在创建 `ThreadPool` 的过程中创建每一个 `Worker` 时需要向 `thread::spawn` 传递一个闭包。
我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行他们。 我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行他们。
在第十六章,我们学习了 **道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: 在第十六章,我们学习了 **道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划:
1. `ThreadPool` 会创建一个道并充当发送端。 1. `ThreadPool` 会创建一个道并充当发送端。
2. 每个 `Worker` 将会充当道的接收端。 2. 每个 `Worker` 将会充当道的接收端。
3. 新建一个 `Job` 结构体来存放用于向道中发送的闭包。 3. 新建一个 `Job` 结构体来存放用于向道中发送的闭包。
4. `execute` 方法会在道发送端发出期望执行的任务。 4. `execute` 方法会在道发送端发出期望执行的任务。
5. 在线程中,`Worker` 会遍历道的接收端并执行任何接收到的任务。 5. 在线程中,`Worker` 会遍历道的接收端并执行任何接收到的任务。
让我们以在 `ThreadPool::new` 中创建道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在道中发出的类型,目前它是一个没有任何内容的结构体: 让我们以在 `ThreadPool::new` 中创建道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在道中发出的类型,目前它是一个没有任何内容的结构体:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -255,11 +255,11 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
{{#rustdoc_include ../listings/ch20-web-server/listing-20-16/src/lib.rs:here}} {{#rustdoc_include ../listings/ch20-web-server/listing-20-16/src/lib.rs:here}}
``` ```
<span class="caption">示例 20-16: 修改 `ThreadPool` 来储存一个发送 `Job` 实例的道发送端</span> <span class="caption">示例 20-16: 修改 `ThreadPool` 来储存一个发送 `Job` 实例的道发送端</span>
`ThreadPool::new` 中,新建了一个道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。 `ThreadPool::new` 中,新建了一个道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。
让我们尝试在线程池创建每个 worker 时将通道的接收端传递给他们。须知我们希望在 worker 所分配的线程中使用通道的接收端,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译: 让我们尝试在线程池创建每个 worker 时将信道的接收端传递给他们。须知我们希望在 worker 所分配的线程中使用信道的接收端,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -267,9 +267,9 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
{{#rustdoc_include ../listings/ch20-web-server/listing-20-17/src/lib.rs:here}} {{#rustdoc_include ../listings/ch20-web-server/listing-20-17/src/lib.rs:here}}
``` ```
<span class="caption">示例 20-17: 将道的接收端传递给 worker</span> <span class="caption">示例 20-17: 将道的接收端传递给 worker</span>
这是一些小而直观的修改:将道的接收端传递进了 `Worker::new`,并接着在闭包中使用它。 这是一些小而直观的修改:将道的接收端传递进了 `Worker::new`,并接着在闭包中使用它。
如果尝试 check 代码,会得到这个错误: 如果尝试 check 代码,会得到这个错误:
@ -277,9 +277,9 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
{{#include ../listings/ch20-web-server/listing-20-17/output.txt}} {{#include ../listings/ch20-web-server/listing-20-17/output.txt}}
``` ```
这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的回忆第十六章Rust 所提供的道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。 这段代码尝试将 `receiver` 传递给多个 `Worker` 实例。这是不行的回忆第十六章Rust 所提供的道实现是多 **生产者**,单 **消费者** 的。这意味着不能简单的克隆道的消费端来解决问题。即便可以,那也不是我们希望使用的技术;我们希望通过在所有的 worker 中共享单一 `receiver`,在线程间分发任务。
另外,从道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。 另外,从道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。
回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc<Mutex<T>>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改: 回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc<Mutex<T>>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改:
@ -289,9 +289,9 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
{{#rustdoc_include ../listings/ch20-web-server/listing-20-18/src/lib.rs:here}} {{#rustdoc_include ../listings/ch20-web-server/listing-20-18/src/lib.rs:here}}
``` ```
<span class="caption">示例 20-18: 使用 `Arc``Mutex` 在 worker 间共享道的接收端</span> <span class="caption">示例 20-18: 使用 `Arc``Mutex` 在 worker 间共享道的接收端</span>
`ThreadPool::new` 中,将道的接收端放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收端的所有权了。 `ThreadPool::new` 中,将道的接收端放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收端的所有权了。
通过这些修改,代码可以编译了!我们做到了! 通过这些修改,代码可以编译了!我们做到了!
@ -305,11 +305,11 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
{{#rustdoc_include ../listings/ch20-web-server/listing-20-19/src/lib.rs:here}} {{#rustdoc_include ../listings/ch20-web-server/listing-20-19/src/lib.rs:here}}
``` ```
<span class="caption">示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在道中发出任务</span> <span class="caption">示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在道中发出任务</span>
在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。 在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。
不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了通道的接收端。相反我们需要闭包一直循环,向通道的接收端请求任务,并在得到任务时执行他们。如示例 20-20 对 `Worker::new` 做出修改: 不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行他们。如示例 20-20 对 `Worker::new` 做出修改:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -321,7 +321,7 @@ pub fn spawn<F, T>(f: F) -> JoinHandle<T>
这里,首先在 `receiver` 上调用了 `lock` 来获取互斥器,接着 `unwrap` 在出现任何错误时 panic。如果互斥器处于一种叫做 **被污染***poisoned*)的状态时获取锁可能会失败,这可能发生于其他线程在持有锁时 panic 了且没有释放锁。在这种情况下,调用 `unwrap` 使其 panic 是正确的行为。请随意将 `unwrap` 改为包含有意义错误信息的 `expect` 这里,首先在 `receiver` 上调用了 `lock` 来获取互斥器,接着 `unwrap` 在出现任何错误时 panic。如果互斥器处于一种叫做 **被污染***poisoned*)的状态时获取锁可能会失败,这可能发生于其他线程在持有锁时 panic 了且没有释放锁。在这种情况下,调用 `unwrap` 使其 panic 是正确的行为。请随意将 `unwrap` 改为包含有意义错误信息的 `expect`
如果锁定了互斥器,接着调用 `recv`道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。 如果锁定了互斥器,接着调用 `recv`道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。
调用 `recv` 会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex<T>` 确保一次只有一个 `Worker` 线程尝试请求任务。 调用 `recv` 会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex<T>` 确保一次只有一个 `Worker` 线程尝试请求任务。

@ -66,7 +66,7 @@
有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为他们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool` ,则主线程会永远阻塞在等待第一个线程结束上。 有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为他们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool` ,则主线程会永远阻塞在等待第一个线程结束上。
为了修复这个问题,修改线程既监听是否有 `Job` 运行也要监听一个应该停止监听并退出无限循环的信号。所以道将发送这个枚举的两个成员之一而不是 `Job` 实例: 为了修复这个问题,修改线程既监听是否有 `Job` 运行也要监听一个应该停止监听并退出无限循环的信号。所以道将发送这个枚举的两个成员之一而不是 `Job` 实例:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -76,7 +76,7 @@
`Message` 枚举要么是存放了线程需要运行的 `Job``NewJob` 成员,要么是会导致线程退出循环并终止的 `Terminate` 成员。 `Message` 枚举要么是存放了线程需要运行的 `Job``NewJob` 成员,要么是会导致线程退出循环并终止的 `Terminate` 成员。
同时需要修改道来使用 `Message` 类型值而不是 `Job`,如示例 20-23 所示: 同时需要修改道来使用 `Message` 类型值而不是 `Job`,如示例 20-23 所示:
<span class="filename">文件名: src/lib.rs</span> <span class="filename">文件名: src/lib.rs</span>
@ -86,7 +86,7 @@
<span class="caption">示例 20-23: 收发 `Message` 值并在 `Worker` 收到 `Message::Terminate` 时退出循环</span> <span class="caption">示例 20-23: 收发 `Message` 值并在 `Worker` 收到 `Message::Terminate` 时退出循环</span>
为了适用 `Message` 枚举需要将两个地方的 `Job` 修改为 `Message``ThreadPool` 的定义和 `Worker::new` 的签名。`ThreadPool` 的 `execute` 方法需要发送封装进 `Message::NewJob` 成员的任务。然后,在 `Worker::new` 中当从道接收 `Message` 时,当获取到 `NewJob`成员会处理任务而收到 `Terminate` 成员则会退出循环。 为了适用 `Message` 枚举需要将两个地方的 `Job` 修改为 `Message``ThreadPool` 的定义和 `Worker::new` 的签名。`ThreadPool` 的 `execute` 方法需要发送封装进 `Message::NewJob` 成员的任务。然后,在 `Worker::new` 中当从道接收 `Message` 时,当获取到 `NewJob`成员会处理任务而收到 `Terminate` 成员则会退出循环。
通过这些修改,代码再次能够编译并继续按照示例 20-20 之后相同的行为运行。不过还是会得到一个警告,因为并没有创建任何 `Terminate` 成员的消息。如示例 20-24 所示修改 `Drop` 实现来修复此问题: 通过这些修改,代码再次能够编译并继续按照示例 20-20 之后相同的行为运行。不过还是会得到一个警告,因为并没有创建任何 `Terminate` 成员的消息。如示例 20-24 所示修改 `Drop` 实现来修复此问题:
@ -98,11 +98,11 @@
<span class="caption">示例 20-24在对每个 worker 线程调用 `join` 之前向 worker 发送 `Message::Terminate`</span> <span class="caption">示例 20-24在对每个 worker 线程调用 `join` 之前向 worker 发送 `Message::Terminate`</span>
现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从道收到终止消息的 worker。 现在遍历了 worker 两次,一次向每个 worker 发送一个 `Terminate` 消息,一个调用每个 worker 线程上的 `join`。如果尝试在同一循环中发送消息并立即 join 线程,则无法保证当前迭代的 worker 是从道收到终止消息的 worker。
为了更好的理解为什么需要两个分开的循环,想象一下只有两个 worker 的场景。如果在一个单独的循环中遍历每个 worker在第一次迭代中向道发出终止消息并对第一个 worker 线程调用 `join`。如果此时第一个 worker 正忙于处理请求,那么第二个 worker 会收到终止消息并停止。我们会一直等待第一个 worker 结束,不过它永远也不会结束因为第二个线程接收了终止消息。死锁! 为了更好的理解为什么需要两个分开的循环,想象一下只有两个 worker 的场景。如果在一个单独的循环中遍历每个 worker在第一次迭代中向道发出终止消息并对第一个 worker 线程调用 `join`。如果此时第一个 worker 正忙于处理请求,那么第二个 worker 会收到终止消息并停止。我们会一直等待第一个 worker 结束,不过它永远也不会结束因为第二个线程接收了终止消息。死锁!
为了避免此情况,首先在一个循环中向道发出所有的 `Terminate` 消息,接着在另一个循环中 join 所有的线程。每个 worker 一旦收到终止消息即会停止从道接收消息,意味着可以确保如果发送同 worker 数相同的终止消息,在 join 之前每个线程都会收到一个终止消息。 为了避免此情况,首先在一个循环中向道发出所有的 `Terminate` 消息,接着在另一个循环中 join 所有的线程。每个 worker 一旦收到终止消息即会停止从道接收消息,意味着可以确保如果发送同 worker 数相同的终止消息,在 join 之前每个线程都会收到一个终止消息。
为了实践这些代码,如示例 20-25 所示修改 `main` 在优雅停机 server 之前只接受两个请求: 为了实践这些代码,如示例 20-25 所示修改 `main` 在优雅停机 server 之前只接受两个请求:
@ -142,7 +142,7 @@ Shutting down worker 3
可能会出现不同顺序的 worker 和信息输出。可以从信息中看到服务是如何运行的: worker 0 和 worker 3 获取了头两个请求,接着在第三个请求时,我们停止接收连接。当 `ThreadPool``main` 的结尾离开作用域时,其 `Drop` 实现开始工作,线程池通知所有线程终止。每个 worker 在收到终止消息时会打印出一个信息,接着线程池调用 `join` 来终止每一个 worker 线程。 可能会出现不同顺序的 worker 和信息输出。可以从信息中看到服务是如何运行的: worker 0 和 worker 3 获取了头两个请求,接着在第三个请求时,我们停止接收连接。当 `ThreadPool``main` 的结尾离开作用域时,其 `Drop` 实现开始工作,线程池通知所有线程终止。每个 worker 在收到终止消息时会打印出一个信息,接着线程池调用 `join` 来终止每一个 worker 线程。
这个特定的运行过程中一个有趣的地方在于:注意我们向道中发出终止消息,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有收到终止消息,所以主线程阻塞直到 worker 0 结束。与此同时,每一个线程都收到了终止消息。一旦 worker 0 结束,主线程就等待其他 worker 结束,此时他们都已经收到终止消息并能够停止了。 这个特定的运行过程中一个有趣的地方在于:注意我们向道中发出终止消息,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有收到终止消息,所以主线程阻塞直到 worker 0 结束。与此同时,每一个线程都收到了终止消息。一旦 worker 0 结束,主线程就等待其他 worker 结束,此时他们都已经收到终止消息并能够停止了。
恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。 恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。

Loading…
Cancel
Save