trpl-zh-cn/src/ch21-03-graceful-shutdown-and-cleanup.md

156 lines
9.3 KiB
Markdown
Raw Normal View History

2018-03-09 17:19:29 +08:00
## 优雅停机与清理
2017-08-10 18:39:22 +08:00
2024-10-20 23:44:05 +08:00
> [ch21-03-graceful-shutdown-and-cleanup.md](https://github.com/rust-lang/book/blob/main/src/ch21-03-graceful-shutdown-and-cleanup.md)
2017-08-10 18:39:22 +08:00
> <br>
2023-01-30 17:39:46 +08:00
> commit 3e5105b52f7e8d3d95def07ffade4dcb1cfdee27
2017-08-10 18:39:22 +08:00
2022-02-10 23:01:01 +08:00
示例 20-20 中的代码如期通过使用线程池异步的响应请求。这里有一些警告说 `workers`、`id` 和 `thread` 字段没有直接被使用,这提醒了我们并没有清理所有的内容。当使用不那么优雅的 <span class="keystroke">ctrl-c</span> 终止主线程时,所有其他线程也会立刻停止,即便它们正处于处理请求的过程中。
2017-08-10 18:39:22 +08:00
现在我们要为 `ThreadPool` 实现 `Drop` trait 对线程池中的每一个线程调用 `join`,这样这些线程将会执行完它们的请求。接着会为 `ThreadPool` 实现一个告诉线程它们应该停止接收新请求并结束的方式。为了实践这些代码,修改 server 在优雅停机graceful shutdown之前只接受两个请求。
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
### 为 `ThreadPool` 实现 `Drop` Trait
现在开始为线程池实现 `Drop`。当线程池被丢弃时,应该 join 所有线程以确保它们完成其操作。示例 20-22 展示了 `Drop` 实现的第一次尝试;这些代码还不能够编译:
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2018-12-09 23:22:10 +08:00
```rust,ignore,does_not_compile
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/listing-21-22/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2022-02-10 23:01:01 +08:00
<span class="caption">示例 20-22: 当线程池离开作用域时 join 每个线程</span>
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
这里首先遍历线程池中的每个 `workers`。这里使用了 `&mut` 因为 `self` 本身是一个可变引用而且也需要能够修改 `worker`。对于每一个线程,会打印出说明信息表明此特定 worker 正在关闭,接着在 worker 线程上调用 `join`。如果 `join` 调用失败,通过 `unwrap` 使得 panic 并进行不优雅的关闭。
2017-08-10 18:39:22 +08:00
如下是尝试编译代码时得到的错误:
2022-02-10 23:01:01 +08:00
```console
2024-10-20 23:44:05 +08:00
{{#include ../listings/ch21-web-server/listing-21-22/output.txt}}
2017-08-10 18:39:22 +08:00
```
这里的错误告诉我们并不能调用 `join`,因为我们只有每一个 `worker` 的可变借用,而 `join` 需要获取其参数的所有权。为了解决这个问题,需要一个方法将 `thread` 移动出拥有其所有权的 `Worker` 实例以便 `join` 可以消费这个线程。示例 17-15 中我们曾见过这么做的方法:如果 `Worker` 存放的是 `Option<thread::JoinHandle<()>`,就可以在 `Option` 上调用 `take` 方法将值从 `Some` 成员中移动出来而对 `None` 成员不做处理。换句话说,正在运行的 `Worker``thread` 将是 `Some` 成员值,而当需要清理 worker 时,将 `Some` 替换为 `None`,这样 worker 就没有可以运行的线程了。
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
为此需要更新 `Worker` 的定义为如下:
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2022-02-10 23:01:01 +08:00
```rust,ignore,does_not_compile
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/no-listing-04-update-worker-definition/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2018-03-09 17:19:29 +08:00
现在依靠编译器来找出其他需要修改的地方。check 代码会得到两个错误:
2017-08-10 18:39:22 +08:00
2022-02-10 23:01:01 +08:00
```console
2024-10-20 23:44:05 +08:00
{{#include ../listings/ch21-web-server/no-listing-04-update-worker-definition/output.txt}}
2017-08-10 18:39:22 +08:00
```
2018-03-09 17:19:29 +08:00
让我们修复第二个错误,它指向 `Worker::new` 结尾的代码;当新建 `Worker` 时需要将 `thread` 值封装进 `Some`。做出如下改变以修复问题:
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
```rust,ignore,does_not_compile
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/no-listing-05-fix-worker-new/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2018-03-09 17:19:29 +08:00
第一个错误位于 `Drop` 实现中。之前提到过要调用 `Option` 上的 `take``thread` 移动出 `worker`。如下改变会修复问题:
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
```rust,ignore,not_desired_behavior
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/no-listing-06-fix-threadpool-drop/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2024-10-20 23:44:05 +08:00
如第十八章我们见过的,`Option` 上的 `take` 方法会取出 `Some` 而留下 `None`。使用 `if let` 解构 `Some` 并得到线程,接着在线程上调用 `join`。如果 worker 的线程已然是 `None`,就知道此时这个 worker 已经清理了其线程所以无需做任何操作。
2018-03-09 17:19:29 +08:00
### 向线程发送信号使其停止接收任务
2017-08-10 18:39:22 +08:00
有了所有这些修改,代码就能编译且没有任何警告。不过也有坏消息,这些代码还不能以我们期望的方式运行。问题的关键在于 `Worker` 中分配的线程所运行的闭包中的逻辑:调用 `join` 并不会关闭线程,因为它们一直 `loop` 来寻找任务。如果采用这个实现来尝试丢弃 `ThreadPool`,则主线程会永远阻塞在等待第一个线程结束上。
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
为了修复这个问题,我们将修改 `ThreadPool``drop` 实现并修改 `Worker` 循环。
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
首先修改 `ThreadPool``drop` 实现在等待线程结束前显式丢弃 `sender`。示例 20-23 展示了 `ThreadPool` 显式丢弃 `sender` 所作的修改。我们使用了与之前处理线程时相同的 `Option``take` 技术以便能从 `ThreadPool` 中移动 `sender`
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
```rust,noplayground,not_desired_behavior
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/listing-21-23/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2023-01-30 17:39:46 +08:00
<span class="caption">示例 20-23: 在 join worker 线程之前显式丢弃 `sender`</span>
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
丢弃 `sender` 会关闭信道,这表明不会有更多的消息被发送。这时 worker 中的无限循环中的所有 `recv` 调用都会返回错误。在示例 20-24 中,我们修改 `Worker` 循环在这种情况下优雅地退出,这意味着当 `ThreadPool``drop` 实现调用 `join` 时线程会结束。
2017-08-10 18:39:22 +08:00
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
```rust,noplayground
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/listing-21-24/src/lib.rs:here}}
2017-08-10 18:39:22 +08:00
```
2023-01-30 17:39:46 +08:00
<span class="caption">示例 20-24`recv` 返回错误时显式退出循环</span>
2017-08-10 18:39:22 +08:00
2022-02-10 23:01:01 +08:00
为了实践这些代码,如示例 20-25 所示修改 `main` 在优雅停机 server 之前只接受两个请求:
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
<span class="filename">文件名src/main.rs</span>
2017-08-10 18:39:22 +08:00
```rust,ignore
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/listing-21-25/src/main.rs:here}}
2017-08-10 18:39:22 +08:00
```
2022-02-10 23:01:01 +08:00
<span class="caption">示例 20-25: 在处理两个请求之后通过退出循环来停止 server</span>
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
你不会希望真实世界的 web server 只处理两次请求就停机了,这只是为了展示优雅停机和清理处于正常工作状态。
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
`take` 方法定义于 `Iterator` trait这里限制循环最多头 2 次。`ThreadPool` 会在 `main` 的结尾离开作用域,而且还会看到 `drop` 实现的运行。
2017-08-10 18:39:22 +08:00
使用 `cargo run` 启动 server并发起三个请求。第三个请求应该会失败而终端的输出应该看起来像这样
2022-02-10 23:01:01 +08:00
```console
2017-08-10 18:39:22 +08:00
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
2022-02-10 23:01:01 +08:00
Finished dev [unoptimized + debuginfo] target(s) in 1.0s
2023-01-30 17:39:46 +08:00
Running `target/debug/hello`
2017-08-10 18:39:22 +08:00
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
2023-01-30 17:39:46 +08:00
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
2017-08-10 18:39:22 +08:00
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
```
2023-01-30 17:39:46 +08:00
可能会出现不同顺序的 worker 和信息输出。可以从信息中看到服务是如何运行的worker 0 和 worker 3 获取了头两个请求。server 会在头第二个请求后停止接受请求,`ThreadPool` 的 `Drop` 实现甚至会在 worker 3 开始工作之前就开始执行。丢弃 `sender` 会断开所有 worker 的连接并让它们关闭。每个 worker 在断开时会打印出一个信息,接着线程池调用 `join` 来等待每一个 worker 线程结束。
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
这个特定的运行过程中一个有趣的地方在于:`ThreadPool` 丢弃 `sender`,而在任何线程收到消息之前,就尝试 join worker 0 了。worker 0 还没有从 `recv` 获得一个错误,所以主线程阻塞直到 worker 0 结束。与此同时worker 3 接收到一个任务接着所有线程会收到一个错误。一旦 worker 0 结束,主线程就等待余下其他 worker 结束。此时它们都退出了循环并停止。
2017-08-10 18:39:22 +08:00
2018-12-09 23:22:10 +08:00
恭喜!现在我们完成了这个项目,也有了一个使用线程池异步响应请求的基础 web server。我们能对 server 执行优雅停机,它会清理线程池中的所有线程。
如下是完整的代码参考:
2017-08-10 18:39:22 +08:00
2023-01-30 17:39:46 +08:00
<span class="filename">文件名src/main.rs</span>
2017-08-10 18:39:22 +08:00
2018-03-09 17:19:29 +08:00
```rust,ignore
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/main.rs}}
2017-08-10 18:39:22 +08:00
```
2023-01-17 11:38:33 +08:00
<span class="filename">文件名src/lib.rs</span>
2017-08-10 18:39:22 +08:00
2022-02-10 23:01:01 +08:00
```rust,noplayground
2024-10-20 23:44:05 +08:00
{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/lib.rs}}
2017-08-10 18:39:22 +08:00
```
这里还有很多可以做的事!如果你希望继续增强这个项目,如下是一些点子:
2022-02-10 23:01:01 +08:00
*`ThreadPool` 和其公有方法增加更多文档
* 为库的功能增加测试
*`unwrap` 调用改为更健壮的错误处理
* 使用 `ThreadPool` 进行其他不同于处理网络请求的任务
* 在 [crates.io](https://crates.io/) 上寻找一个线程池 crate 并使用它实现一个类似的 web server将其 API 和鲁棒性与我们的实现做对比
2017-08-10 18:39:22 +08:00
## 总结
好极了!你结束了本书的学习!由衷感谢你同我们一道加入这次 Rust 之旅。现在你已经准备好出发并实现自己的 Rust 项目并帮助他人了。请不要忘记我们的社区,这里有其他 Rustaceans 正乐于帮助你迎接 Rust 之路上的任何挑战。