Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help


x-i18n: generated_at: “2026-03-01T15:07:21Z” model: gemini-3-flash-preview provider: google-gemini-cli source_hash: e4c2abaaa676697bc84f66443a4f10ee3ebd19f305e5edcda888e5872ab7ff92 source_path: ch21-03-graceful-shutdown-and-cleanup.md workflow: 16

优雅停机与清理 (Graceful Shutdown and Cleanup)

示例 21-20 中的代码正如我们所愿,通过使用线程池来异步响应请求。我们得到了一些关于未以直接方式使用的 workersidthread 字段的警告,这提醒我们没有进行任何清理工作。当我们使用不那么优雅的 ctrl-C 方法来停止主线程时,所有其他线程也会立即停止,即使它们正处于服务请求的中途。

The code in Listing 21-20 is responding to requests asynchronously through the use of a thread pool, as we intended. We get some warnings about the workers, id, and thread fields that we’re not using in a direct way that reminds us we’re not cleaning up anything. When we use the less elegant ctrl-C method to halt the main thread, all other threads are stopped immediately as well, even if they’re in the middle of serving a request.

那么接下来,我们将实现 Drop 特征来对池中的每个线程调用 join ,以便它们在关闭之前可以完成正在处理的请求。然后,我们将实现一种方法来告诉线程它们应该停止接收新请求并关闭。为了看到这段代码的实际运行,我们将修改我们的服务器,使其在优雅地关闭其线程池之前仅接受两个请求。

Next, then, we’ll implement the Drop trait to call join on each of the threads in the pool so that they can finish the requests they’re working on before closing. Then, we’ll implement a way to tell the threads they should stop accepting new requests and shut down. To see this code in action, we’ll modify our server to accept only two requests before gracefully shutting down its thread pool.

在进行过程中要注意一点:这一切都不会影响处理执行闭包的那部分代码,所以如果我们在异步运行时中使用线程池,这里的一切都会是一样的。

One thing to notice as we go: None of this affects the parts of the code that handle executing the closures, so everything here would be the same if we were using a thread pool for an async runtime.

ThreadPool 上实现 Drop 特征 (Implementing the Drop Trait on ThreadPool)

Implementing the Drop Trait on ThreadPool

让我们从在我们的线程池上实现 Drop 开始。当池被丢弃时,我们的线程应该全部 join 以确保它们完成工作。示例 21-22 展示了 Drop 实现的第一次尝试;这段代码目前还不能完全运行。

Let’s start with implementing Drop on our thread pool. When the pool is dropped, our threads should all join to make sure they finish their work. Listing 21-22 shows a first attempt at a Drop implementation; this code won’t quite work yet.

{{#rustdoc_include ../listings/ch21-web-server/listing-21-22/src/lib.rs:here}}

首先,我们遍历线程池中的每一个 workers 。我们为此使用 &mut ,因为 self 是一个可变引用,并且我们也需要能够修改 worker 。对于每个 worker ,我们打印一条消息表示该特定的 Worker 实例正在关闭,然后我们在该 Worker 实例的线程上调用 join 。如果对 join 的调用失败,我们使用 unwrap 让 Rust 引发恐慌并进入非优雅停机。

First, we loop through each of the thread pool workers. We use &mut for this because self is a mutable reference, and we also need to be able to mutate worker. For each worker, we print a message saying that this particular Worker instance is shutting down, and then we call join on that Worker instance’s thread. If the call to join fails, we use unwrap to make Rust panic and go into an ungraceful shutdown.

这是我们编译这段代码时得到的错误:

Here is the error we get when we compile this code:

{{#include ../listings/ch21-web-server/listing-21-22/output.txt}}

该错误告诉我们不能调用 join ,因为我们只有每个 worker 的可变借用,而 join 会获取其参数的所有权。为了解决此问题,我们需要将线程从拥有 threadWorker 实例中移出,以便 join 可以消耗该线程。实现这一点的一种方法是采取我们在示例 18-15 中采取的相同方法。如果 Worker 持有一个 Option<thread::JoinHandle<()>> ,我们就可以在 Option 上调用 take 方法将值从 Some 变体中移出,并在原位留下一个 None 变体。换句话说,正在运行的 Workerthread 中会有一个 Some 变体,而当我们想清理 Worker 时,我们会用 None 替换 Some ,这样 Worker 就没有可以运行的线程了。

The error tells us we can’t call join because we only have a mutable borrow of each worker and join takes ownership of its argument. To solve this issue, we need to move the thread out of the Worker instance that owns thread so that join can consume the thread. One way to do this is to take the same approach we took in Listing 18-15. If Worker held an Option<thread::JoinHandle<()>>, we could call the take method on the Option to move the value out of the Some variant and leave a None variant in its place. In other words, a Worker that is running would have a Some variant in thread, and when we wanted to clean up a Worker, we’d replace Some with None so that the Worker wouldn’t have a thread to run.

然而,出现这种情况的“唯一”时刻是在丢弃 Worker 时。作为代价,我们在访问 worker.thread 的任何地方都必须处理 Option<thread::JoinHandle<()>> 。惯用的 Rust 经常使用 Option ,但当你发现自己为了像这样绕过问题而将一个你明知始终存在的东西包装在 Option 中时,寻找替代方法来使你的代码更简洁且更不易出错是一个好主意。

However, the only time this would come up would be when dropping the Worker. In exchange, we’d have to deal with an Option<thread::JoinHandle<()>> anywhere we accessed worker.thread. Idiomatic Rust uses Option quite a bit, but when you find yourself wrapping something you know will always be present in an Option as a workaround like this, it’s a good idea to look for alternative approaches to make your code cleaner and less error-prone.

在这种情况下,存在一个更好的替代方案: Vec::drain 方法。它接收一个范围参数来指定要从向量中移除哪些项,并返回这些项的迭代器。传入 .. 范围语法将移除向量中的“每一项”。

In this case, a better alternative exists: the Vec::drain method. It accepts a range parameter to specify which items to remove from the vector and returns an iterator of those items. Passing the .. range syntax will remove every value from the vector.

所以,我们需要像这样更新 ThreadPooldrop 实现:

So, we need to update the ThreadPool drop implementation like this:

#![allow(unused)]
fn main() {
{{#rustdoc_include ../listings/ch21-web-server/no-listing-04-update-drop-definition/src/lib.rs:here}}
}

这解决了编译器错误,并且不需要对我们的代码进行任何其他更改。请注意,因为 drop 可以在恐慌时被调用, unwrap 也可能会引发恐慌并导致双重恐慌,这将立即导致程序崩溃并结束正在进行的任何清理。这对于示例程序来说是可以的,但不建议用于生产代码。

This resolves the compiler error and does not require any other changes to our code. Note that, because drop can be called when panicking, the unwrap could also panic and cause a double panic, which immediately crashes the program and ends any cleanup in progress. This is fine for an example program, but it isn’t recommended for production code.

通知线程停止监听作业 (Signaling to the Threads to Stop Listening for Jobs)

Signaling to the Threads to Stop Listening for Jobs

做了所有这些更改后,我们的代码在编译时没有任何警告。然而,坏消息是,这段代码目前还不能按照我们想要的方式运作。关键在于由 Worker 实例线程运行的闭包中的逻辑:目前,我们调用了 join ,但这不会关闭线程,因为它们在 loop 中永远寻找作业。如果我们尝试使用当前的 drop 实现来丢弃我们的 ThreadPool ,主线程将永远阻塞,等待第一个线程结束。

With all the changes we’ve made, our code compiles without any warnings. However, the bad news is that this code doesn’t function the way we want it to yet. The key is the logic in the closures run by the threads of the Worker instances: At the moment, we call join, but that won’t shut down the threads, because they loop forever looking for jobs. If we try to drop our ThreadPool with our current implementation of drop, the main thread will block forever, waiting for the first thread to finish.

为了解决这个问题,我们需要在 ThreadPooldrop 实现中做一处改动,然后在 Worker 循环中也做一处改动。

To fix this problem, we’ll need a change in the ThreadPool drop implementation and then a change in the Worker loop.

首先,我们将更改 ThreadPooldrop 实现,在等待线程结束之前显式丢弃 sender 。示例 21-23 显示了对 ThreadPool 进行的显式丢弃 sender 的更改。与线程不同,这里我们“确实”需要使用 Option 才能通过 Option::takesender 移出 ThreadPool

First, we’ll change the ThreadPool drop implementation to explicitly drop the sender before waiting for the threads to finish. Listing 21-23 shows the changes to ThreadPool to explicitly drop sender. Unlike with the thread, here we do need to use an Option to be able to move sender out of ThreadPool with Option::take.

{{#rustdoc_include ../listings/ch21-web-server/listing-21-23/src/lib.rs:here}}

丢弃 sender 会关闭通道,这表示不再会有消息被发送。当发生这种情况时, Worker 实例在无限循环中执行的所有 recv 调用都将返回一个错误。在示例 21-24 中,我们将 Worker 循环更改为在这种情况下优雅地退出循环,这意味着当 ThreadPooldrop 实现对线程调用 join 时,线程将会结束。

Dropping sender closes the channel, which indicates no more messages will be sent. When that happens, all the calls to recv that the Worker instances do in the infinite loop will return an error. In Listing 21-24, we change the Worker loop to gracefully exit the loop in that case, which means the threads will finish when the ThreadPool drop implementation calls join on them.

{{#rustdoc_include ../listings/ch21-web-server/listing-21-24/src/lib.rs:here}}

为了看到这段代码的实际运行,让我们修改 main 以便在优雅地关闭服务器之前仅接受两个请求,如示例 21-25 所示。

To see this code in action, let’s modify main to accept only two requests before gracefully shutting down the server, as shown in Listing 21-25.

{{#rustdoc_include ../listings/ch21-web-server/listing-21-25/src/main.rs:here}}

你肯定不希望一个现实世界的 Web 服务器在仅服务两个请求后就关闭。这段代码只是演示优雅停机和清理工作处于正常状态。

You wouldn’t want a real-world web server to shut down after serving only two requests. This code just demonstrates that the graceful shutdown and cleanup is in working order.

take 方法定义在 Iterator 特征中,它最多将迭代限制在前两个项。 ThreadPool 将在 main 结束时超出作用域,并运行 drop 实现。

The take method is defined in the Iterator trait and limits the iteration to the first two items at most. The ThreadPool will go out of scope at the end of main, and the drop implementation will run.

使用 cargo run 启动服务器并发起三个请求。第三个请求应该会报错,在你的终端中,你应该看到类似于这样的输出:

Start the server with cargo run and make three requests. The third request should error, and in your terminal, you should see output similar to this:

$ cargo run
   Compiling hello v0.1.0 (file:///projects/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
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.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

你可能会看到不同的 Worker ID 排序和打印的消息顺序。我们可以从消息中看到这段代码是如何工作的: Worker 实例 0 和 3 获得了前两个请求。服务器在第二个连接之后停止接受连接,并且 ThreadPool 上的 Drop 实现甚至在 Worker 3 开始其作业之前就开始执行。丢弃 sender 会断开所有 Worker 实例的连接并告诉它们关闭。 Worker 实例在断开连接时各自打印一条消息,然后线程池调用 join 等待每个 Worker 线程结束。

You might see a different ordering of Worker IDs and messages printed. We can see how this code works from the messages: Worker instances 0 and 3 got the first two requests. The server stopped accepting connections after the second connection, and the Drop implementation on ThreadPool starts executing before Worker 3 even starts its job. Dropping the sender disconnects all the Worker instances and tells them to shut down. The Worker instances each print a message when they disconnect, and then the thread pool calls join to wait for each Worker thread to finish.

注意这次特定执行的一个有趣方面: ThreadPool 丢弃了 sender ,并且在任何 Worker 收到错误之前,我们就尝试 join Worker 0Worker 0 尚未从 recv 收到错误,因此主线程发生阻塞,等待 Worker 0 结束。与此同时, Worker 3 收到了一个作业,然后所有线程都收到了一个错误。当 Worker 0 完成后,主线程等待剩余的 Worker 实例完成。在那一点上,它们都已经退出了循环并停止了。

Notice one interesting aspect of this particular execution: The ThreadPool dropped the sender, and before any Worker received an error, we tried to join Worker 0. Worker 0 had not yet gotten an error from recv, so the main thread blocked, waiting for Worker 0 to finish. In the meantime, Worker 3 received a job and then all threads received an error. When Worker 0 finished, the main thread waited for the rest of the Worker instances to finish. At that point, they had all exited their loops and stopped.

恭喜!我们现在已经完成了我们的项目;我们拥有一个使用线程池异步响应的基本 Web 服务器。我们能够执行服务器的优雅停机,清理池中的所有线程。

Congrats! We’ve now completed our project; we have a basic web server that uses a thread pool to respond asynchronously. We’re able to perform a graceful shutdown of the server, which cleans up all the threads in the pool.

这是供参考的完整代码:

Here’s the full code for reference:

{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/main.rs}}
{{#rustdoc_include ../listings/ch21-web-server/no-listing-07-final-code/src/lib.rs}}

我们在这里还可以做得更多!如果你想继续增强这个项目,这里有一些想法:

  • ThreadPool 及其公共方法添加更多文档。
  • 添加对库功能的测试。
  • 将对 unwrap 的调用更改为更健壮的错误处理。
  • 使用 ThreadPool 执行除服务 Web 请求之外的其他任务。
  • crates.io 上寻找一个线程池 crate,并改为使用该 crate 实现一个类似的 Web 服务器。然后,将其 API 和健壮性与我们实现的线程池进行比较。

We could do more here! If you want to continue enhancing this project, here are some ideas:

  • Add more documentation to ThreadPool and its public methods.
  • Add tests of the library’s functionality.
  • Change calls to unwrap to more robust error handling.
  • Use ThreadPool to perform some task other than serving web requests.
  • Find a thread pool crate on crates.io and implement a similar web server using the crate instead. Then, compare its API and robustness to the thread pool we implemented.

总结 (Summary)

Summary

做得好!你已经读到了这本书的尽头!我们要感谢你加入我们的 Rust 之旅。你现在已经准备好实现你自己的 Rust 项目并帮助他人的项目了。请记住,有一个热情的其他 Rustaceans 社区,他们非常乐意帮助你解决在 Rust 旅程中遇到的任何挑战。

Well done! You’ve made it to the end of the book! We want to thank you for joining us on this tour of Rust. You’re now ready to implement your own Rust projects and help with other people’s projects. Keep in mind that there is a welcoming community of other Rustaceans who would love to help you with any challenges you encounter on your Rust journey.