From 3c4a7a5ee598c6b2c56e2dfecd3c8debcebd6b0e Mon Sep 17 00:00:00 2001 From: juicyenc Date: Fri, 22 Apr 2022 22:44:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=94=B9channel=E7=9A=84=E7=BF=BB?= =?UTF-8?q?=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ch16-00-concurrency.md | 2 +- src/ch16-02-message-passing.md | 38 ++++++++--------- src/ch16-03-shared-state.md | 4 +- ...04-extensible-concurrency-sync-and-send.md | 2 +- src/ch20-02-multithreaded.md | 42 +++++++++---------- src/ch20-03-graceful-shutdown-and-cleanup.md | 14 +++---- 6 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/ch16-00-concurrency.md b/src/ch16-00-concurrency.md index ecd5861..7d2319a 100644 --- a/src/ch16-00-concurrency.md +++ b/src/ch16-00-concurrency.md @@ -14,6 +14,6 @@ 如下是本章将要涉及到的内容: * 如何创建线程来同时运行多段代码。 -* **消息传递**(_Message passing_)并发,其中通道(channel)被用来在线程间传递消息。 +* **消息传递**(_Message passing_)并发,其中信道(channel)被用来在线程间传递消息。 * **共享状态**(_Shared state_)并发,其中多个线程可以访问同一片数据。 * `Sync` 和 `Send` trait,将 Rust 的并发保证扩展到用户定义的以及标准库提供的类型中。 diff --git a/src/ch16-02-message-passing.md b/src/ch16-02-message-passing.md index eebc9c5..cf24921 100644 --- a/src/ch16-02-message-passing.md +++ b/src/ch16-02-message-passing.md @@ -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.”) -Rust 中一个实现消息传递并发的主要工具是 **通道**(_channel_),Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的通道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。 +Rust 中一个实现消息传递并发的主要工具是 **信道**(_channel_),Rust 标准库提供了其实现的编程概念。你可以将其想象为一个水流的渠道,比如河流或小溪。如果你将诸如橡皮鸭或小船之类的东西放入其中,它们会顺流而下到达下游。 -编程中的通道有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。发送者位于上游位置,在这里可以将橡皮鸭放入河中,接收者则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为通道被 **关闭**(_closed_)了。 +编程中的信息渠道(信道)有两部分组成,一个发送者(transmitter)和一个接收者(receiver)。发送者位于上游位置,在这里可以将橡皮鸭放入河中,接收者则位于下游,橡皮鸭最终会漂流至此。代码中的一部分调用发送者的方法以及希望发送的数据,另一部分则检查接收端收到的消息。当发送者或接收者任一被丢弃时可以认为信道被 **关闭**(_closed_)了。 -这里,我们将开发一个程序,它会在一个线程生成值向通道发送,而在另一个线程会接收值并打印出来。这里会通过通道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用通道来实现聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。 +这里,我们将开发一个程序,它会在一个线程生成值向信道发送,而在另一个线程会接收值并打印出来。这里会通过信道在线程间发送简单值来演示这个功能。一旦你熟悉了这项技术,就能使用信道来实现聊天系统,或利用很多线程进行分布式计算并将部分计算结果发送给一个线程进行聚合。 -首先,在示例 16-6 中,创建了一个通道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在通道中发送什么类型: +首先,在示例 16-6 中,创建了一个信道但没有做任何事。注意这还不能编译,因为 Rust 不知道我们想要在信道中发送什么类型: 文件名: src/main.rs @@ -19,9 +19,9 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**(_channel_ {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-06/src/main.rs}} ``` -示例 16-6: 创建一个通道,并将其两端赋值给 `tx` 和 `rx` +示例 16-6: 创建一个信道,并将其两端赋值给 `tx` 和 `rx` -这里使用 `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` 返回的元组中一部分的手段。 @@ -35,11 +35,11 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**(_channel_ 示例 16-7: 将 `tx` 移动到一个新建的线程中并发送 “hi” -这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有通道的发送端以便能向通道发送消息。 +这里再次使用 `thread::spawn` 来创建一个新线程并使用 `move` 将 `tx` 移动到闭包中这样新建线程就拥有 `tx` 了。新建线程需要拥有信道的发送端以便能向信道发送消息。 -通道的发送端有一个 `send` 方法用来获取需要放入通道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。 +信道的发送端有一个 `send` 方法用来获取需要放入信道的值。`send` 方法返回一个 `Result` 类型,所以如果接收端已经被丢弃了,将没有发送值的目标,所以发送操作会返回错误。在这个例子中,出错的时候调用 `unwrap` 产生 panic。不过对于一个真实程序,需要合理地处理它:回到第九章复习正确处理错误的策略。 -在示例 16-8 中,我们在主线程中从通道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: +在示例 16-8 中,我们在主线程中从信道的接收端获取值。这类似于在河的下游捞起橡皮鸭或接收聊天信息: 文件名: src/main.rs @@ -49,7 +49,7 @@ Rust 中一个实现消息传递并发的主要工具是 **通道**(_channel_ 示例 16-8: 在主线程中接收并打印内容 “hi” -通道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从通道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当通道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 +信道的接收端有两个有用的方法:`recv` 和 `try_recv`。这里,我们使用了 `recv`,它是 _receive_ 的缩写。这个方法会阻塞主线程执行直到从信道中接收一个值。一旦发送了一个值,`recv` 会在一个 `Result` 中返回它。当信道发送端关闭,`recv` 会返回一个错误表明不会再有新的值到来了。 `try_recv` 不会阻塞,相反它立刻返回一个 `Result`:`Ok` 值包含可用的信息,而 `Err` 值代表此时没有任何消息。如果线程在等待消息过程中还有其他工作时使用 `try_recv` 很有用:可以编写一个循环来频繁调用 `try_recv`,在有可用消息时进行处理,其余时候则处理一会其他工作直到再次检查。 @@ -63,9 +63,9 @@ Got: hi 完美! -### 通道与所有权转移 +### 信道与所有权转移 -所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个试验来看看通道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的通道中发送完 `val` 值 **之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: +所有权规则在消息传递中扮演了重要角色,其有助于我们编写安全的并发代码。防止并发编程中的错误是在 Rust 程序中考虑所有权的一大优势。现在让我们做一个试验来看看信道与所有权如何一同协作以避免产生问题:我们将尝试在新建线程中的信道中发送完 `val` 值 **之后** 再使用它。尝试编译示例 16-9 中的代码并看看为何这是不允许的: 文件名: src/main.rs @@ -73,9 +73,9 @@ Got: hi {{#rustdoc_include ../listings/ch16-fearless-concurrency/listing-16-09/src/main.rs}} ``` -示例 16-9: 在我们已经发送到通道中后,尝试使用 `val` 引用 +示例 16-9: 在我们已经发送到信道中后,尝试使用 `val` 引用 -这里尝试在通过 `tx.send` 发送 `val` 到通道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时,Rust 会给出一个错误: +这里尝试在通过 `tx.send` 发送 `val` 到信道中之后将其打印出来。允许这么做是一个坏主意:一旦将值发送到另一个线程后,那个线程可能会在我们再次使用它之前就将其修改或者丢弃。其他线程对值可能的修改会由于不一致或不存在的数据而导致错误或意外的结果。然而,尝试编译示例 16-9 的代码时,Rust 会给出一个错误: ```console {{#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 中的代码是并发执行的:新建线程现在会发送多个消息并在每个消息之间暂停一秒钟。 文件名: src/main.rs @@ -97,7 +97,7 @@ Got: hi 这一次,在新建线程中有一个字符串 vector 希望发送到主线程。我们遍历他们,单独的发送每一个字符串并通过一个 `Duration` 值调用 `thread::sleep` 函数来暂停一秒。 -在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当通道被关闭时,迭代器也将结束。 +在主线程中,不再显式调用 `recv` 函数:而是将 `rx` 当作一个迭代器。对于每一个接收到的值,我们将其打印出来。当信道被关闭时,迭代器也将结束。 当运行示例 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 所示: 文件名: src/main.rs @@ -122,7 +122,7 @@ Got: thread 示例 16-11: 从多个生产者发送多个消息 -这一次,在创建新线程之前,我们对通道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的通道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向通道的接收端发送不同的消息。 +这一次,在创建新线程之前,我们对信道的发送端调用了 `clone` 方法。这会给我们一个可以传递给第一个新建线程的发送端句柄。我们会将原始的信道发送端传递给第二个新建线程。这样就会有两个线程,每个线程将向信道的接收端发送不同的消息。 如果运行这些代码,你 **可能** 会看到这样的输出: @@ -139,4 +139,4 @@ Got: you 虽然你可能会看到这些值以不同的顺序出现;这依赖于你的系统。这也就是并发既有趣又困难的原因。如果通过 `thread::sleep` 做实验,在不同的线程中提供不同的值,就会发现他们的运行更加不确定,且每次都会产生不同的输出。 -现在我们见识过了通道如何工作,再看看另一种不同的并发方式吧。 +现在我们见识过了信道如何工作,再看看另一种不同的并发方式吧。 diff --git a/src/ch16-03-shared-state.md b/src/ch16-03-shared-state.md index 7940d0c..28ace97 100644 --- a/src/ch16-03-shared-state.md +++ b/src/ch16-03-shared-state.md @@ -9,7 +9,7 @@ > > 通过共享内存通讯看起来如何?除此之外,为何消息传递的拥护者并不使用它并反其道而行之呢? -在某种程度上,任何编程语言中的通道都类似于单所有权,因为一旦将一个值传送到通道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 +在某种程度上,任何编程语言中的信道都类似于单所有权,因为一旦将一个值传送到信道中,将无法再使用这个值。共享内存类似于多所有权:多个线程可以同时访问相同的内存位置。第十五章介绍了智能指针如何使得多所有权成为可能,然而这会增加额外的复杂性,因为需要以某种方式管理这些不同的所有者。Rust 的类型系统和所有权规则极大的协助了正确地管理这些所有权。作为一个例子,让我们看看互斥器,一个更为常见的共享内存并发原语。 ### 互斥器一次只允许一个线程访问数据 @@ -22,7 +22,7 @@ 作为一个现实中互斥器的例子,想象一下在某个会议的一次小组座谈会中,只有一个麦克风。如果一位成员要发言,他必须请求或表示希望使用麦克风。一旦得到了麦克风,他可以畅所欲言,然后将麦克风交给下一位希望讲话的成员。如果一位成员结束发言后忘记将麦克风交还,其他人将无法发言。如果对共享麦克风的管理出现了问题,座谈会将无法如期进行! -正确的管理互斥器异常复杂,这也是许多人之所以热衷于通道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。 +正确的管理互斥器异常复杂,这也是许多人之所以热衷于信道的原因。然而,在 Rust 中,得益于类型系统和所有权,我们不会在锁和解锁上出错。 ### `Mutex`的 API diff --git a/src/ch16-04-extensible-concurrency-sync-and-send.md b/src/ch16-04-extensible-concurrency-sync-and-send.md index 936e154..e34aad4 100644 --- a/src/ch16-04-extensible-concurrency-sync-and-send.md +++ b/src/ch16-04-extensible-concurrency-sync-and-send.md @@ -33,7 +33,7 @@ Rust 的并发模型中一个有趣的方面是:语言本身对并发知之 ** 正如之前提到的,因为 Rust 本身很少有处理并发的部分内容,有很多的并发方案都由 crate 实现。他们比标准库要发展的更快;请在网上搜索当前最新的用于多线程场景的 crate。 -Rust 提供了用于消息传递的通道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! +Rust 提供了用于消息传递的信道,和像 `Mutex` 和 `Arc` 这样可以安全的用于并发上下文的智能指针。类型系统和借用检查器会确保这些场景中的代码,不会出现数据竞争和无效的引用。一旦代码可以编译了,我们就可以坚信这些代码可以正确的运行于多线程环境,而不会出现其他语言中经常出现的那些难以追踪的 bug。并发编程不再是什么可怕的概念:无所畏惧地并发吧! 接下来,让我们讨论一下当 Rust 程序变得更大时,有哪些符合语言习惯的问题建模方法和结构化解决方案,以及 Rust 的风格是如何与面向对象编程(Object Oriented Programming)中那些你所熟悉的概念相联系的。 diff --git a/src/ch20-02-multithreaded.md b/src/ch20-02-multithreaded.md index 588e895..a7d2ebe 100644 --- a/src/ch20-02-multithreaded.md +++ b/src/ch20-02-multithreaded.md @@ -233,21 +233,21 @@ pub fn spawn(f: F) -> JoinHandle 这段代码能够编译并用指定给 `ThreadPool::new` 的参数创建储存了一系列的 `Worker` 实例,不过 **仍然** 没有处理 `execute` 中得到的闭包。让我们聊聊接下来怎么做。 -#### 使用通道向线程发送请求 +#### 使用信道向线程发送请求 下一个需要解决的问题是传递给 `thread::spawn` 的闭包完全没有做任何工作。目前,我们在 `execute` 方法中获得期望执行的闭包,不过在创建 `ThreadPool` 的过程中创建每一个 `Worker` 时需要向 `thread::spawn` 传递一个闭包。 我们希望刚创建的 `Worker` 结构体能够从 `ThreadPool` 的队列中获取需要执行的代码,并发送到线程中执行他们。 -在第十六章,我们学习了 **通道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里通道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: +在第十六章,我们学习了 **信道** —— 一个沟通两个线程的简单手段 —— 对于这个例子来说则是绝佳的。这里信道将充当任务队列的作用,`execute` 将通过 `ThreadPool` 向其中线程正在寻找工作的 `Worker` 实例发送任务。如下是这个计划: -1. `ThreadPool` 会创建一个通道并充当发送端。 -2. 每个 `Worker` 将会充当通道的接收端。 -3. 新建一个 `Job` 结构体来存放用于向通道中发送的闭包。 -4. `execute` 方法会在通道发送端发出期望执行的任务。 -5. 在线程中,`Worker` 会遍历通道的接收端并执行任何接收到的任务。 +1. `ThreadPool` 会创建一个信道并充当发送端。 +2. 每个 `Worker` 将会充当信道的接收端。 +3. 新建一个 `Job` 结构体来存放用于向信道中发送的闭包。 +4. `execute` 方法会在信道发送端发出期望执行的任务。 +5. 在线程中,`Worker` 会遍历信道的接收端并执行任何接收到的任务。 -让我们以在 `ThreadPool::new` 中创建通道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在通道中发出的类型,目前它是一个没有任何内容的结构体: +让我们以在 `ThreadPool::new` 中创建信道并让 `ThreadPool` 实例充当发送端开始,如示例 20-16 所示。`Job` 是将在信道中发出的类型,目前它是一个没有任何内容的结构体: 文件名: src/lib.rs @@ -255,11 +255,11 @@ pub fn spawn(f: F) -> JoinHandle {{#rustdoc_include ../listings/ch20-web-server/listing-20-16/src/lib.rs:here}} ``` -示例 20-16: 修改 `ThreadPool` 来储存一个发送 `Job` 实例的通道发送端 +示例 20-16: 修改 `ThreadPool` 来储存一个发送 `Job` 实例的信道发送端 -在 `ThreadPool::new` 中,新建了一个通道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。 +在 `ThreadPool::new` 中,新建了一个信道,并接着让线程池在接收端等待。这段代码能够编译,不过仍有警告。 -让我们尝试在线程池创建每个 worker 时将通道的接收端传递给他们。须知我们希望在 worker 所分配的线程中使用通道的接收端,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译: +让我们尝试在线程池创建每个 worker 时将信道的接收端传递给他们。须知我们希望在 worker 所分配的线程中使用信道的接收端,所以将在闭包中引用 `receiver` 参数。示例 20-17 中展示的代码还不能编译: 文件名: src/lib.rs @@ -267,9 +267,9 @@ pub fn spawn(f: F) -> JoinHandle {{#rustdoc_include ../listings/ch20-web-server/listing-20-17/src/lib.rs:here}} ``` -示例 20-17: 将通道的接收端传递给 worker +示例 20-17: 将信道的接收端传递给 worker -这是一些小而直观的修改:将通道的接收端传递进了 `Worker::new`,并接着在闭包中使用它。 +这是一些小而直观的修改:将信道的接收端传递进了 `Worker::new`,并接着在闭包中使用它。 如果尝试 check 代码,会得到这个错误: @@ -277,9 +277,9 @@ pub fn spawn(f: F) -> JoinHandle {{#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>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 20-18 展示了所需的修改: @@ -289,9 +289,9 @@ pub fn spawn(f: F) -> JoinHandle {{#rustdoc_include ../listings/ch20-web-server/listing-20-18/src/lib.rs:here}} ``` -示例 20-18: 使用 `Arc` 和 `Mutex` 在 worker 间共享通道的接收端 +示例 20-18: 使用 `Arc` 和 `Mutex` 在 worker 间共享信道的接收端 -在 `ThreadPool::new` 中,将通道的接收端放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker,克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收端的所有权了。 +在 `ThreadPool::new` 中,将信道的接收端放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker,克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收端的所有权了。 通过这些修改,代码可以编译了!我们做到了! @@ -305,11 +305,11 @@ pub fn spawn(f: F) -> JoinHandle {{#rustdoc_include ../listings/ch20-web-server/listing-20-19/src/lib.rs:here}} ``` -示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在通道中发出任务 +示例 20-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在信道中发出任务 -在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从通道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。 +在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从信道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在他们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。 -不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了通道的接收端。相反我们需要闭包一直循环,向通道的接收端请求任务,并在得到任务时执行他们。如示例 20-20 对 `Worker::new` 做出修改: +不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行他们。如示例 20-20 对 `Worker::new` 做出修改: 文件名: src/lib.rs @@ -321,7 +321,7 @@ pub fn spawn(f: F) -> JoinHandle 这里,首先在 `receiver` 上调用了 `lock` 来获取互斥器,接着 `unwrap` 在出现任何错误时 panic。如果互斥器处于一种叫做 **被污染**(*poisoned*)的状态时获取锁可能会失败,这可能发生于其他线程在持有锁时 panic 了且没有释放锁。在这种情况下,调用 `unwrap` 使其 panic 是正确的行为。请随意将 `unwrap` 改为包含有意义错误信息的 `expect`。 -如果锁定了互斥器,接着调用 `recv` 从通道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有通道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。 +如果锁定了互斥器,接着调用 `recv` 从信道中接收 `Job`。最后的 `unwrap` 也绕过了一些错误,这可能发生于持有信道发送端的线程停止的情况,类似于如果接收端关闭时 `send` 方法如何返回 `Err` 一样。 调用 `recv` 会阻塞当前线程,所以如果还没有任务,其会等待直到有可用的任务。`Mutex` 确保一次只有一个 `Worker` 线程尝试请求任务。 diff --git a/src/ch20-03-graceful-shutdown-and-cleanup.md b/src/ch20-03-graceful-shutdown-and-cleanup.md index 6530cbb..6b9c2cf 100644 --- a/src/ch20-03-graceful-shutdown-and-cleanup.md +++ b/src/ch20-03-graceful-shutdown-and-cleanup.md @@ -66,7 +66,7 @@ 有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为他们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool` ,则主线程会永远阻塞在等待第一个线程结束上。 -为了修复这个问题,修改线程既监听是否有 `Job` 运行也要监听一个应该停止监听并退出无限循环的信号。所以通道将发送这个枚举的两个成员之一而不是 `Job` 实例: +为了修复这个问题,修改线程既监听是否有 `Job` 运行也要监听一个应该停止监听并退出无限循环的信号。所以信道将发送这个枚举的两个成员之一而不是 `Job` 实例: 文件名: src/lib.rs @@ -76,7 +76,7 @@ `Message` 枚举要么是存放了线程需要运行的 `Job` 的 `NewJob` 成员,要么是会导致线程退出循环并终止的 `Terminate` 成员。 -同时需要修改通道来使用 `Message` 类型值而不是 `Job`,如示例 20-23 所示: +同时需要修改信道来使用 `Message` 类型值而不是 `Job`,如示例 20-23 所示: 文件名: src/lib.rs @@ -86,7 +86,7 @@ 示例 20-23: 收发 `Message` 值并在 `Worker` 收到 `Message::Terminate` 时退出循环 -为了适用 `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` 实现来修复此问题: @@ -98,11 +98,11 @@ 示例 20-24:在对每个 worker 线程调用 `join` 之前向 worker 发送 `Message::Terminate` -现在遍历了 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 之前只接受两个请求: @@ -142,7 +142,7 @@ Shutting down worker 3 可能会出现不同顺序的 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 执行优雅停机,它会清理线程池中的所有线程。