diff --git a/src/ch21-02-multithreaded.md b/src/ch21-02-multithreaded.md index a2f9076..095879b 100644 --- a/src/ch21-02-multithreaded.md +++ b/src/ch21-02-multithreaded.md @@ -112,7 +112,7 @@ {{#rustdoc_include ../listings/ch21-web-server/no-listing-02-impl-threadpool-new/src/lib.rs}} ``` -这里选择 `usize` 作为 `size` 参数的类型,因为我们知道为负的线程数没有意义。我们还知道将使用 4 作为线程集合的元素数量,这也就是使用 `usize` 类型的原因,如第三章 [“整型”][integer-types] 部分所讲。 +这里选择 `usize` 作为 `size` 参数的类型,因为我们知道线程数为负没有意义。我们还知道将使用 `4` 作为线程集合的元素数量,这也就是使用 `usize` 类型的原因,如第三章 [“整型”][integer-types] 部分所讲。 再次编译检查这段代码: @@ -120,7 +120,7 @@ {{#include ../listings/ch21-web-server/no-listing-02-impl-threadpool-new/output.txt}} ``` -现在有了一个警告和一个错误。暂时先忽略警告,发生错误是因为并没有 `ThreadPool` 上的 `execute` 方法。回忆 [“创建有限数量的线程”](#创建有限数量的线程) 部分我们决定线程池应该有与 `thread::spawn` 类似的接口,同时我们将实现 `execute` 函数来获取传递的闭包并将其传递给池中的空闲线程执行。 +这里发生错误是因为并没有 `ThreadPool` 上的 `execute` 方法。回忆 [“创建有限数量的线程”](#创建有限数量的线程) 部分我们决定线程池应该有与 `thread::spawn` 类似的接口,同时我们将实现 `execute` 函数来获取传递的闭包并将其传递给池中的空闲线程执行。 我们会在 `ThreadPool` 上定义 `execute` 函数来获取一个闭包参数。回忆第十三章的 [“将被捕获的值移出闭包和 `Fn` trait”][fn-traits] 部分,闭包作为参数时可以使用三个不同的 trait:`Fn`、`FnMut` 和 `FnOnce`。我们需要决定这里应该使用哪种闭包。最终需要实现的类似于标准库的 `thread::spawn`,所以我们可以观察 `thread::spawn` 的签名在其参数中使用了何种 bound。查看文档会发现: @@ -152,11 +152,13 @@ pub fn spawn(f: F) -> JoinHandle 现在就只有警告了!这意味着能够编译了!注意如果尝试 `cargo run` 运行程序并在浏览器中发起请求,仍会在浏览器中出现在本章开始时那样的错误。这个库实际上还没有调用传递给 `execute` 的闭包! -> 一个你可能听说过的关于像 Haskell 和 Rust 这样有严格编译器的语言的说法是 “如果代码能够编译,它就能工作”。这是一个提醒大家的好时机,实际上这并不是普适的。我们的项目可以编译,不过它完全没有做任何工作!如果构建一个真实且功能完整的项目,则需花费大量的时间来开始编写单元测试来检查代码能否编译 **并且** 拥有期望的行为。 +> 一个你可能听说过的关于像 Haskell 和 Rust 这样有严格编译器的语言的说法是 “如果代码能够编译,它就能工作”。不过这个说法并不是普适的。我们的项目可以编译,不过它完全没有做任何工作!如果构建一个真实且功能完整的项目,则需花费大量的时间来开始编写单元测试来检查代码能否编译 **并且** 拥有期望的行为。 -#### 在 `new` 中验证池中线程数量 +思考一下:如果这里要执行的是一个 `future` 而不是闭包会有什么不同? -这里仍然存在警告是因为其并没有对 `new` 和 `execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `usize` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic,如示例 21-13 所示: +#### 在 `new` 中验证线程池的线程数量 + +这里并没有对 `new` 和 `execute` 的参数做任何操作。让我们用期望的行为来实现这些函数。以考虑 `new` 作为开始。之前选择使用无符号类型作为 `size` 参数的类型,因为线程数为负的线程池没有意义。然而,线程数为零的线程池同样没有意义,不过零是一个完全有效的 `usize` 值。让我们增加在返回 `ThreadPool` 实例之前检查 `size` 是否大于零的代码,并使用 `assert!` 宏在得到零时 panic,如示例 21-13 所示: 文件名:src/lib.rs @@ -174,9 +176,9 @@ pub fn spawn(f: F) -> JoinHandle pub fn build(size: usize) -> Result { ``` -#### 分配空间以储存线程 +#### 分配空间以存储线程 -现在有了一个有效的线程池线程数,就可以实际创建这些线程并在返回结构体之前将它们储存在 `ThreadPool` 结构体中。不过如何 “储存” 一个线程?让我们再看看 `thread::spawn` 的签名: +现在我们已经有了一种方法来确保线程池中的线程数有效,就可以实际创建这些线程并在返回结构体之前将它们存储在 `ThreadPool` 结构体中。不过如何 “存储” 一个线程?让我们再看看 `thread::spawn` 的签名: ```rust,ignore pub fn spawn(f: F) -> JoinHandle @@ -204,24 +206,26 @@ pub fn spawn(f: F) -> JoinHandle 如果再次运行 `cargo check`,它应该会成功。 -#### `Worker` 结构体负责从 `ThreadPool` 中将代码传递给线程 +#### `Worker` 结构体负责将代码从 `ThreadPool` 传递给线程 + +示例 21-14 的 `for` 循环中留下了一个关于创建线程的注释。这里,我们来看看如何实际创建线程。标准库提供了 `thread::spawn` 作为创建线程的方法,`thread::spawn` 期望获取一些一旦创建线程就应该执行的代码。然而,我们希望开始线程并使其等待稍后传递的代码。标准库的线程实现并没有包含这么做的方法;我们必须手动实现。 -示例 21-14 的 `for` 循环中留下了一个关于创建线程的注释。如何实际创建线程呢?这是一个难题。标准库提供的创建线程的方法,`thread::spawn`,它期望获取一些一旦创建线程就应该执行的代码。然而,我们希望开始线程并使其等待稍后传递的代码。标准库的线程实现并没有包含这么做的方法;我们必须自己实现。 +我们将要实现的行为是创建线程并稍后发送代码,这会在 `ThreadPool` 和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 *Worker*,这是一个池实现中的常见概念。`Worker` 会获取需要运行的代码,并在该 worker 的线程中运行该代码。 -我们将要实现的行为是创建线程并稍后发送代码,这会在 `ThreadPool` 和线程间引入一个新数据类型来管理这种新行为。这个数据结构称为 *Worker*,这是一个池实现中的常见概念。想象一下在餐馆厨房工作的员工:员工等待来自客户的订单,他们负责接受这些订单并完成它们。 +想象一下在餐馆厨房工作的员工:员工等待来自顾客的订单,他们负责接单并完成它们。 -不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector,我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在 `Worker` 上实现一个方法,该方法将闭包发送到已经运行的线程中执行。我们还会赋予每一个 worker `id`,这样就可以在日志和调试中区别线程池中的不同 worker。 +不同于在线程池中储存一个 `JoinHandle<()>` 实例的 vector,我们会储存 `Worker` 结构体的实例。每一个 `Worker` 会储存一个单独的 `JoinHandle<()>` 实例。接着会在 `Worker` 上实现一个方法,该方法将闭包发送到已经运行的线程中执行。我们还会赋予每个 worker 一个 `id`,这样就可以在日志和调试中区别线程池中的不同 `Worker` 的实例。 如下是创建 `ThreadPool` 时会发生的新过程。在通过如下方式设置完 `Worker` 之后,我们会实现向线程发送闭包的代码: -1. 定义 `Worker` 结构体存放 `id` 和 `JoinHandle<()>` -2. 修改 `ThreadPool` 存放一个 `Worker` 实例的 vector -3. 定义 `Worker::new` 函数,它获取一个 `id` 数字并返回一个带有 `id` 和用空闭包分配的线程的 `Worker` 实例 -4. 在 `ThreadPool::new` 中,使用 `for` 循环计数生成 `id`,使用这个 `id` 新建 `Worker`,并储存进 vector 中 +1. 定义存放 `id` 和 `JoinHandle<()>` 的 `Worker` 结构体。 +2. 修改 `ThreadPool` 存放一个 `Worker` 实例的 vector。 +3. 定义 `Worker::new` 函数,它获取一个 `id` 数字并返回一个带有 `id` 和用空闭包分配的线程的 `Worker` 实例。 +4. 在 `ThreadPool::new` 中,使用 `for` 循环计数生成 `id`,使用这个 `id` 新建 `Worker`,并储存进 vector 中。 如果你渴望挑战,在查示例 21-15 中的代码之前尝试自己实现这些修改。 -准备好了吗?示例 21-15 就是一个做出了这些修改的例子: +准备好了吗?示例 21-15 就是一个做出了上述修改的例子: 文件名:src/lib.rs @@ -231,29 +235,29 @@ pub fn spawn(f: F) -> JoinHandle 示例 21-15: 修改 `ThreadPool` 存放 `Worker` 实例而不是直接存放线程 -这里将 `ThreadPool` 中字段名从 `threads` 改为 `workers`,因为它现在储存 `Worker` 而不是 `JoinHandle<()>`。使用 `for` 循环中的计数作为 `Worker::new` 的参数,并将每一个新建的 `Worker` 储存在叫做 `workers` 的 vector 中。 +这里将 `ThreadPool` 中字段名从 `threads` 改为 `workers`,因为它现在存储 `Worker` 而不是 `JoinHandle<()>`。使用 `for` 循环中的计数作为 `Worker::new` 的参数,并将每一个新建的 `Worker` 存储在叫做 `workers` 的 vector 中。 -`Worker` 结构体和其 `new` 函数是私有的,因为外部代码(比如 *src/main.rs* 中的 server)并不需要知道关于 `ThreadPool` 中使用 `Worker` 结构体的实现细节。`Worker::new` 函数使用 `id` 参数并储存了使用一个空闭包创建的 `JoinHandle<()>`。 +`Worker` 结构体和其 `new` 函数是私有的,因为外部代码(比如 *src/main.rs* 中的 server)并不需要知道关于 `ThreadPool` 中使用 `Worker` 结构体的实现细节。`Worker::new` 函数使用 `id` 参数并存储了使用一个空闭包创建的 `JoinHandle<()>` 实例。 > 注意:如果操作系统因为没有足够的系统资源而无法创建线程时,`thread::spawn` 会 panic。这会导致整个 server panic,即使一些线程可能创建成功了。出于简单的考虑,这个行为是可行的,不过在一个生产级别的线程池实现中,你可能会希望使用 [`std::thread::Builder`][builder] 和其 [`spawn`][builder-spawn] 方法来返回一个 `Result`。 -这段代码能够编译并用指定给 `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` 会创建一个信道并充当发送者。 -2. 每个 `Worker` 将会充当接收者。 +1. `ThreadPool` 会创建一个信道并持有发送端。 +2. 每个 `Worker` 将持有接收端。 3. 新建一个 `Job` 结构体来存放用于向信道中发送的闭包。 -4. `execute` 方法会在发送者发出期望执行的任务。 -5. 在线程中,`Worker` 会遍历接收者并执行任何接收到的任务。 +4. `execute` 方法会在发送者发出期望执行的工作。 +5. 在线程中,`Worker` 会遍历接收者并执行任何接收到的工作。 -让我们以在 `ThreadPool::new` 中创建信道并让 `ThreadPool` 实例充当发送者开始,如示例 21-16 所示。`Job` 是将在信道中发出的类型,目前它是一个没有任何内容的结构体: +让我们以在 `ThreadPool::new` 中创建信道并让 `ThreadPool` 实例充当发送者开始,如示例 21-16 所示。`Job` 结构体目前为空,但它将作为我们通过通道发送的类型: 文件名:src/lib.rs @@ -265,7 +269,7 @@ pub fn spawn(f: F) -> JoinHandle 在 `ThreadPool::new` 中,新建了一个信道,并接着让线程池在接收端等待。这段代码能够成功编译。 -让我们尝试在线程池创建每个 worker 时将接收者传递给它们。须知我们希望在 worker 所分配的线程中使用接收者,所以将在闭包中引用 `receiver` 参数。示例 21-17 中展示的代码还不能编译: +让我们尝试在线程池创建每个 worker 时将接收端传递给它们。须知我们希望在 worker 所分配的线程中使用接收者,所以将在闭包中引用 `receiver` 参数。示例 21-17 中展示的代码还不能编译: 文件名:src/lib.rs @@ -275,7 +279,7 @@ pub fn spawn(f: F) -> JoinHandle 示例 21-17: 将信道的接收端传递给 worker -这是一些小而直观的修改:将接收者传递进了 `Worker::new`,并接着在闭包中使用它。 +这是一些简单而直观的修改:将接收端传递进了 `Worker::new`,并接着在闭包中使用它。 如果尝试 check 代码,会得到这个错误: @@ -287,7 +291,7 @@ pub fn spawn(f: F) -> JoinHandle 另外,从信道队列中取出任务涉及到修改 `receiver`,所以这些线程需要一个能安全的共享和修改 `receiver` 的方式,否则可能导致竞争状态(参考第十六章)。 -回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 worker 拥有接收端,而 `Mutex` 则确保一次只有一个 worker 能从接收端得到任务。示例 21-18 展示了所需的修改: +回忆一下第十六章讨论的线程安全智能指针,为了在多个线程间共享所有权并允许线程修改其值,需要使用 `Arc>`。`Arc` 使得多个 `Worker` 实例拥有接收端,而 `Mutex` 则确保一次只有一个 `Worker` 能从接收端得到任务。示例 21-18 展示了所需的修改: 文件名:src/lib.rs @@ -297,9 +301,9 @@ pub fn spawn(f: F) -> JoinHandle 示例 21-18: 使用 `Arc` 和 `Mutex` 在 worker 间共享接收者 -在 `ThreadPool::new` 中,将接收者放入一个 `Arc` 和一个 `Mutex` 中。对于每一个新 worker,克隆 `Arc` 来增加引用计数,如此这些 worker 就可以共享接收者的所有权了。 +在 `ThreadPool::new` 中,将接收端放入 `Arc` 和 `Mutex` 中。对于每一个新 `Worker` `Arc` 来增加引用计数,如此这些 `Worker` 实例就可以共享接收者的所有权了。 -通过这些修改,代码可以编译了!我们做到了! +通过这些修改,代码可以编译了!我们已经快完成了! #### 实现 `execute` 方法 @@ -313,9 +317,9 @@ pub fn spawn(f: F) -> JoinHandle 示例 21-19: 为存放每一个闭包的 `Box` 创建一个 `Job` 类型别名,接着在信道中发出任务 -在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从信道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在它们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,即便编译器不这么认为。 +在使用 `execute` 得到的闭包新建 `Job` 实例之后,将这些任务从信道的发送端发出。这里调用 `send` 上的 `unwrap`,因为发送可能会失败,这可能发生于例如停止了所有线程执行的情况,这意味着接收端停止接收新消息了。不过目前我们无法停止线程执行;只要线程池存在它们就会一直执行。使用 `unwrap` 是因为我们知道失败不可能发生,不过编译器不知道这些。 -不过到此事情还没有结束!在 worker 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行它们。如示例 21-20 对 `Worker::new` 做出修改: +不过到此事情还没有结束!在 `Worker` 中,传递给 `thread::spawn` 的闭包仍然还只是 **引用** 了信道的接收端。相反我们需要闭包一直循环,向信道的接收端请求任务,并在得到任务时执行它们。如示例 21-20 对 `Worker::new` 做出修改: 文件名:src/lib.rs @@ -336,28 +340,28 @@ pub fn spawn(f: F) -> JoinHandle ```console $ cargo run Compiling hello v0.1.0 (file:///projects/hello) -warning: field is never read: `workers` +warning: field `workers` is never read --> src/lib.rs:7:5 | +6 | pub struct ThreadPool { + | ---------- field in this struct 7 | workers: Vec, - | ^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^^ | = note: `#[warn(dead_code)]` on by default -warning: field is never read: `id` +warning: fields `id` and `thread` are never read --> src/lib.rs:48:5 | +47 | struct Worker { + | ------ fields in this struct 48 | id: usize, - | ^^^^^^^^^ - -warning: field is never read: `thread` - --> src/lib.rs:49:5 - | + | ^^ 49 | thread: thread::JoinHandle<()>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | ^^^^^^ -warning: `hello` (lib) generated 3 warnings - Finished dev [unoptimized + debuginfo] target(s) in 1.40s +warning: `hello` (lib) generated 2 warnings + Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s Running `target/debug/hello` Worker 0 got a job; executing. Worker 2 got a job; executing. @@ -371,9 +375,9 @@ Worker 0 got a job; executing. Worker 2 got a job; executing. ``` -成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当 server 收到大量请求时系统也不会负担过重。如果请求 */sleep*,server 也能够通过另外一个线程处理其他请求。 +成功了!现在我们有了一个可以异步执行连接的线程池!它绝不会创建超过四个线程,所以当服务端收到大量请求时系统也不会负担过重。如果请求 */sleep*,server 也能够通过另外一个线程处理其他请求。 -> 注意如果同时在多个浏览器窗口打开 */sleep*,它们可能会彼此间隔地加载 5 秒,因为一些浏览器出于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 web server 造成的。 +> 注意如果同时在多个浏览器窗口打开 */sleep*,它们可能会彼此间隔地加载 5 秒,因为一些浏览器出于缓存的原因会顺序执行相同请求的多个实例。这些限制并不是由于我们的 web 服务端造成的。 在学习了第十七章和第十八章的 `while let` 循环之后,你可能会好奇为何不能如此编写 worker 线程,如示例 21-21 所示: @@ -387,10 +391,10 @@ Worker 2 got a job; executing. 这段代码可以编译和运行,但是并不会产生所期望的线程行为:一个慢请求仍然会导致其他请求等待执行。其原因有些微妙:`Mutex` 结构体没有公有 `unlock` 方法,因为锁的所有权依赖 `lock` 方法返回的 `LockResult>` 中 `MutexGuard` 的生命周期。这允许借用检查器在编译时确保绝不会在没有持有锁的情况下访问由 `Mutex` 守护的资源,不过如果没有认真的思考 `MutexGuard` 的生命周期的话,也可能会导致比预期更久的持有锁。 -示例 21-20 中的代码使用的 `let job = receiver.lock().unwrap().recv().unwrap();` 之所以可以工作是因为对于 `let` 来说,当 `let` 语句结束时任何表达式中等号右侧使用的临时值都会立即被丢弃。然而 `while let`(`if let` 和 `match`)直到相关的代码块结束都不会丢弃临时值。在示例 21-21 中,`job()` 调用期间锁一直持续,这也意味着其他的 worker 无法接受任务。 +示例 21-20 中的代码使用的 `let job = receiver.lock().unwrap().recv().unwrap();` 之所以可以工作是因为对于 `let` 来说,当 `let` 语句结束时任何表达式中等号右侧使用的临时值都会立即被丢弃。然而 `while let`(`if let` 和 `match`)直到相关的代码块结束都不会丢弃临时值。在示例 21-21 中,`job()` 调用期间锁一直持续,这也意味着其他的 `Worker` 实例无法接收任务。 [creating-type-synonyms-with-type-aliases]: -ch21-03-advanced-types.html#使用类型别名创建类型同义词 +ch20-03-advanced-types.html#使用类型别名创建类型同义词 [integer-types]: ch03-02-data-types.html#整型 [fn-traits]: ch13-01-closures.html#将被捕获的值移出闭包和-fn-trait [builder]: https://doc.rust-lang.org/std/thread/struct.Builder.html diff --git a/src/ch21-03-graceful-shutdown-and-cleanup.md b/src/ch21-03-graceful-shutdown-and-cleanup.md index 51dddbd..9aab6ec 100644 --- a/src/ch21-03-graceful-shutdown-and-cleanup.md +++ b/src/ch21-03-graceful-shutdown-and-cleanup.md @@ -2,15 +2,17 @@ > [ch21-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/main/src/ch21-03-graceful-shutdown-and-cleanup.md) >
-> commit 3e5105b52f7e8d3d95def07ffade4dcb1cfdee27 +> commit 5d22a358fb2380aa3f270d7b6269b67b8e44849e -示例 20-20 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 ctrl-c 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。 +示例 21-20 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 ctrl-c 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。 -现在我们要为 `ThreadPool` 实现 `Drop` trait 对线程池中的每一个线程调用 `join`,这样这些线程将会执行完它们的请求。接着会为 `ThreadPool` 实现一个告诉线程它们应该停止接收新请求并结束的方式。为了实践这些代码,修改 server 在优雅停机(graceful shutdown)之前只接受两个请求。 +现在我们要为 `ThreadPool` 实现 `Drop` trait 对线程池中的每一个线程调用 `join`,这样这些线程在关闭前将会执行完它们的请求。接着会为 `ThreadPool` 实现一个告诉线程它们应该停止接收新请求并结束的方式。为了实践这些代码,修改服务端在优雅停机(graceful shutdown)之前只接受两个请求。 + +在我们开始时需要注意的是:这一切都不会影响处理执行闭包的那部分代码因此如果我们在异步运行时中使用线程池,所有操作也完全相同。 ### 为 `ThreadPool` 实现 `Drop` Trait -现在开始为线程池实现 `Drop`。当线程池被丢弃时,应该 join 所有线程以确保它们完成其操作。示例 20-22 展示了 `Drop` 实现的第一次尝试;这些代码还不能够编译: +现在开始为线程池实现 `Drop`。当线程池被丢弃时,应该 join 所有线程以确保它们完成其操作。示例 21-22 展示了 `Drop` 实现的第一次尝试;这些代码还不能够编译: 文件名:src/lib.rs @@ -18,57 +20,37 @@ {{#rustdoc_include ../listings/ch21-web-server/listing-21-22/src/lib.rs:here}} ``` -示例 20-22: 当线程池离开作用域时 join 每个线程 - -这里首先遍历线程池中的每个 `workers`。这里使用了 `&mut` 因为 `self` 本身是一个可变引用而且也需要能够修改 `worker`。对于每一个线程,会打印出说明信息表明此特定 worker 正在关闭,接着在 worker 线程上调用 `join`。如果 `join` 调用失败,通过 `unwrap` 使得 panic 并进行不优雅的关闭。 +示例 21-22: 当线程池离开作用域时 join 每个线程 -如下是尝试编译代码时得到的错误: +这里首先遍历线程池中的每个 `workers`。这里使用了 `&mut` 因为 `self` 本身是一个可变引用而且也需要能够修改 `worker`。对于每一个线程,会打印出说明信息表明此特定 `Worker` 实例正在关闭,接着在 `Worker` 实例的线程上调用 `join`。如果 `join` 调用失败,通过 `unwrap` 使得 panic 并进行不优雅的关闭。 ```console {{#include ../listings/ch21-web-server/listing-21-22/output.txt}} ``` -这里的错误告诉我们并不能调用 `join`,因为我们只有每一个 `worker` 的可变借用,而 `join` 需要获取其参数的所有权。为了解决这个问题,需要一个方法将 `thread` 移动出拥有其所有权的 `Worker` 实例以便 `join` 可以消费这个线程。示例 17-15 中我们曾见过这么做的方法:如果 `Worker` 存放的是 `Option`,就可以在 `Option` 上调用 `take` 方法将值从 `Some` 成员中移动出来而对 `None` 成员不做处理。换句话说,正在运行的 `Worker` 的 `thread` 将是 `Some` 成员值,而当需要清理 worker 时,将 `Some` 替换为 `None`,这样 worker 就没有可以运行的线程了。 - -为此需要更新 `Worker` 的定义为如下: - -文件名:src/lib.rs +这里的错误告诉我们并不能调用 `join`,因为我们只有每一个 `worker` 的可变借用,而 `join` 需要获取其参数的所有权。为了解决这个问题,需要一个方法将 `thread` 移动出拥有其所有权的 `Worker` 实例以便 `join` 可以消费这个线程。示例 18-15 中我们曾见过这么做的方法:如果 `Worker` 存放的是 `Option`,就可以在 `Option` 上调用 `take` 方法将值从 `Some` 成员中移动出来而对 `None` 成员不做处理。换句话说,正在运行的 `Worker` 的 `thread` 将是 `Some` 成员值,而当需要清理 worker 时,将 `Some` 替换为 `None`,这样 worker 就没有可以运行的线程了。 -```rust -{{#rustdoc_include ../listings/ch21-web-server/no-listing-04-update-drop-definition/src/lib.rs:here}} -``` +然而,这种情况**只**会在丢弃 `Worker` 时出现。相应地,我们必须在任何访问 `worker.thread` 的处理 `Option>`。在惯用的 Rust 代码中 `Option` 用的很多,但当你发现自己总是知道 `Option` 中一定会有值,却还要将其包装在 `Option` 中来应对这一场景时,就应该考虑其他更优雅的方法了。 -现在依靠编译器来找出其他需要修改的地方。check 代码会得到两个错误: +在这个例子中,存在一个更好的替代方案:`Vec::drain` 方法。它接受一个 range 参数来指定哪些项要从 `Vec` 中移除,并返回一个这些项的迭代器。使用 `..` range 语法会从 `Vec` 中移除**所有**值。 -```console -{{#include ../listings/ch21-web-server/no-listing-04-update-worker-definition/output.txt}} -``` - -让我们修复第二个错误,它指向 `Worker::new` 结尾的代码;当新建 `Worker` 时需要将 `thread` 值封装进 `Some`。做出如下改变以修复问题: +因此我们需要像下面这样更新 `ThreadPool` 的 `drop` 实现: 文件名:src/lib.rs -```rust,ignore,does_not_compile -{{#rustdoc_include ../listings/ch21-web-server/no-listing-05-fix-worker-new/src/lib.rs:here}} -``` - -第一个错误位于 `Drop` 实现中。之前提到过要调用 `Option` 上的 `take` 将 `thread` 移动出 `worker`。如下改变会修复问题: - -文件名:src/lib.rs - -```rust,ignore,not_desired_behavior -{{#rustdoc_include ../listings/ch21-web-server/no-listing-06-fix-threadpool-drop/src/lib.rs:here}} +```rust +{{#rustdoc_include ../listings/ch21-web-server/no-listing-04-update-drop-definition/src/lib.rs:here}} ``` -如第十八章我们见过的,`Option` 上的 `take` 方法会取出 `Some` 而留下 `None`。使用 `if let` 解构 `Some` 并得到线程,接着在线程上调用 `join`。如果 worker 的线程已然是 `None`,就知道此时这个 worker 已经清理了其线程所以无需做任何操作。 +这解决了编译器错误且不需要对我们的代码做其它更改。 ### 向线程发送信号使其停止接收任务 -有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为它们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool`,则主线程会永远阻塞在等待第一个线程结束上。 +有了所有这些修改,代码就能编译且没有任何警告。然而,坏消息是,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 实例中分配的线程所运行的闭包中的逻辑:此时,调用 `join` 并不会关闭线程,因为它们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool`,则主线程会永远阻塞在等待第一个线程结束上。 为了修复这个问题,我们将修改 `ThreadPool` 的 `drop` 实现并修改 `Worker` 循环。 -首先修改 `ThreadPool` 的 `drop` 实现在等待线程结束前显式丢弃 `sender`。示例 20-23 展示了 `ThreadPool` 显式丢弃 `sender` 所作的修改。我们使用了与之前处理线程时相同的 `Option` 和 `take` 技术以便能从 `ThreadPool` 中移动 `sender`: +首先修改 `ThreadPool` 的 `drop` 实现在等待线程结束前显式地丢弃 `sender`。示例 21-23 展示了 `ThreadPool` 显式丢弃 `sender` 所作的修改。与处理线程时不同,这里**确实**需要使用 `Option`,以便能够使用 `Option::take` 将 `sender` 从 `ThreadPool` 中移出。 文件名:src/lib.rs @@ -76,9 +58,9 @@ {{#rustdoc_include ../listings/ch21-web-server/listing-21-23/src/lib.rs:here}} ``` -示例 20-23: 在 join worker 线程之前显式丢弃 `sender` +示例 21-23: 在 join `Worker` 线程之前显式丢弃 `sender` -丢弃 `sender` 会关闭信道,这表明不会有更多的消息被发送。这时 worker 中的无限循环中的所有 `recv` 调用都会返回错误。在示例 20-24 中,我们修改 `Worker` 循环在这种情况下优雅地退出,这意味着当 `ThreadPool` 的 `drop` 实现调用 `join` 时线程会结束。 +丢弃 `sender` 会关闭信道,这表明不会有更多的消息被发送。这时 `Worker` 实例中的无限循环中的所有 `recv` 调用都会返回错误。在示例 21-24 中,我们修改 `Worker` 循环在这种情况下优雅地退出,这意味着当 `ThreadPool` 的 `drop` 实现调用 `join` 时线程会结束。 文件名:src/lib.rs @@ -86,9 +68,9 @@ {{#rustdoc_include ../listings/ch21-web-server/listing-21-24/src/lib.rs:here}} ``` -示例 20-24:当 `recv` 返回错误时显式退出循环 +示例 21-24:当 `recv` 返回错误时显式退地出循环 -为了实践这些代码,如示例 20-25 所示修改 `main` 在优雅停机 server 之前只接受两个请求: +为了实践这些代码,如示例 21-25 所示修改 `main` 在优雅停机服务端之前只接受两个请求: 文件名:src/main.rs @@ -96,18 +78,18 @@ {{#rustdoc_include ../listings/ch21-web-server/listing-21-25/src/main.rs:here}} ``` -示例 20-25: 在处理两个请求之后通过退出循环来停止 server +示例 21-25: 在处理两个请求之后通过退出循环来停止服务端 -你不会希望真实世界的 web server 只处理两次请求就停机了,这只是为了展示优雅停机和清理处于正常工作状态。 +你不会希望真实世界的 web 服务端只处理两次请求就停机了,这只是为了展示优雅停机和清理处于正常工作状态。 -`take` 方法定义于 `Iterator` trait,这里限制循环最多头 2 次。`ThreadPool` 会在 `main` 的结尾离开作用域,而且还会看到 `drop` 实现的运行。 +`take` 方法定义于 `Iterator` trait,这里限制循环最多头 2 次。`ThreadPool` 会在 `main` 的结尾离开作用域, `drop` 实现会运行。 -使用 `cargo run` 启动 server,并发起三个请求。第三个请求应该会失败,而终端的输出应该看起来像这样: +使用 `cargo run` 启动服务端,并发起三个请求。第三个请求应该会失败,而终端的输出应该看起来像这样: ```console $ cargo run Compiling hello v0.1.0 (file:///projects/hello) - Finished dev [unoptimized + debuginfo] target(s) in 1.0s + Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/hello` Worker 0 got a job; executing. Shutting down. @@ -122,11 +104,11 @@ Shutting down worker 2 Shutting down worker 3 ``` -可能会出现不同顺序的 worker 和信息输出。可以从信息中看到服务是如何运行的:worker 0 和 worker 3 获取了头两个请求。server 会在头第二个请求后停止接受请求,`ThreadPool` 的 `Drop` 实现甚至会在 worker 3 开始工作之前就开始执行。丢弃 `sender` 会断开所有 worker 的连接并让它们关闭。每个 worker 在断开时会打印出一个信息,接着线程池调用 `join` 来等待每一个 worker 线程结束。 +可能会出现不同顺序的 `Worker` ID 和信息输出。可以从信息中看到服务是如何运行的:`Worker` 实例 0 和 3 获取了头两个请求。server 会在头第二个请求后停止接受请求,`ThreadPool` 的 `Drop` 实现甚至会在 `Worker` 3 开始工作之前就开始执行。丢弃 `sender` 会断开所有 `Worker` 实例的连接并让它们关闭。每个 `Worker` 实例在断开时会打印出一个信息,接着线程池调用 `join` 来等待每一个 `Worker` 线程结束。 -这个特定的运行过程中一个有趣的地方在于:`ThreadPool` 丢弃 `sender`,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有从 `recv` 获得一个错误,所以主线程阻塞直到 worker 0 结束。与此同时,worker 3 接收到一个任务接着所有线程会收到一个错误。一旦 worker 0 结束,主线程就等待余下其他 worker 结束。此时它们都退出了循环并停止。 +注意在这个特定的运行过程中一个有趣的地方在于:`ThreadPool` 丢弃 `sender`,而在任何 `Worker` 收到消息之前,就尝试 join `Worker` 0 `Worker` 0 还没有从 `recv` 获得一个错误,所以主线程阻塞直到 `Worker` 0 结束。与此同时,`Worker` 3 接收到一个任务接着所有线程会收到一个错误。一旦 `Worker` 0 结束,主线程就等待余下其他 worker 结束。此时它们都退出了循环并停止。 -恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。 +恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web 服务端。我们能对服务端执行优雅停机,它会清理线程池中的所有线程。 如下是完整的代码参考: @@ -142,14 +124,14 @@ Shutting down worker 3 {{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/lib.rs}} ``` -这里还有很多可以做的事!如果你希望继续增强这个项目,如下是一些点子: +我们还能做得更多!如果你希望继续增强这个项目,如下是一些点子: * 为 `ThreadPool` 和其公有方法增加更多文档 * 为库的功能增加测试 * 将 `unwrap` 调用改为更健壮的错误处理 * 使用 `ThreadPool` 进行其他不同于处理网络请求的任务 -* 在 [crates.io](https://crates.io/) 上寻找一个线程池 crate 并使用它实现一个类似的 web server,将其 API 和鲁棒性与我们的实现做对比 +* 在 [crates.io](https://crates.io/) 上寻找一个线程池 crate 并使用它实现一个类似的 web 服务端,将其 API 和鲁棒性与我们的实现做对比 ## 总结 -好极了!你结束了本书的学习!由衷感谢你同我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。 +好极了!你已经完成了本书的学习!由衷感谢你与我们一道踏上这段 Rust 之旅。现在你已经准备好实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。