键盘快捷键

按 ← 或 → 在章节之间导航

按 S 或 / 在本书中搜索

按 ? 显示此帮助

按 Esc 隐藏此帮助

优雅停机与清理

示例 21-20 中的代码正如我们所期望的那样,通过线程池异步地响应请求。我们收到了一些关于 workers、id 和 thread 字段没有以直接方式被使用的警告,这提醒我们 并没有清理任何内容。当我们使用不那么优雅的 ctrl-C 方式来终止主线程时,所有其他线程 也会被立即停止,即使它们正在处理请求的过程中。

接下来,我们将实现 Drop trait,对线程池中的每个 线程调用 join,使它们能够在关闭前完成正在处理的请求。 然后,我们将实现一种方式来告知线程们应该停止接受新请求并关闭。 为了看到这段代码的实际效果,我们将修改服务器,使其在优雅地关闭 线程池之前只接受两个请求。

在这个过程中需要注意的一点是:这些改动都不会影响处理闭包执行的那部分 代码,所以如果我们使用线程池来运行异步运行时,这里的所有内容也完全一样。

为 ThreadPool 实现 Drop Trait

让我们从为线程池实现 Drop 开始。当线程池被丢弃时, 所有线程都应该 join,以确保它们完成自己的工作。 示例 21-22 展示了 Drop 实现的第一次尝试;这段代码还不能 完全正常工作。

文件名:src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
示例 21-22:当线程池离开作用域时 join 每个线程

首先,我们遍历线程池中的每个 workers。这里使用 &mut, 因为 self 是一个可变引用,而且我们还需要能够修改 worker。对于每个 worker,我们打印一条消息说明这个特定的 Worker 实例正在关闭,然后调用该 Worker 实例的线程上的 join。如果 join 调用失败,我们通过 unwrap 让 Rust panic,进入不优雅的关闭流程。

编译这段代码时会得到如下错误:

$ cargo check
    Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
  --> src/lib.rs:52:13
   |
52 |             worker.thread.join().unwrap();
   |             ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
   |             |
   |             move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
   |
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
  --> /rustc/2d8144b7880597b6e6d3dfd63a9a9efae3f533d3/library/std/src/thread/join_handle.rs:149:16

For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error

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

然而,这种技巧唯一会用到的场合就是丢弃 Worker 的时候。作为交换,我们不得不在任何访问 worker.thread 的地方 都处理 Option<thread::JoinHandle<()>>。 地道的 Rust 代码中会大量使用 Option,但当你发现自己像这样 把一个明知总会存在的值包进 Option 作为变通手段时, 最好寻找其他方案,让代码更简洁、更不容易出错。

在这个例子中,存在一个更好的替代方案:Vec::drain 方法。它接受 一个范围参数来指定要从 vector 中移除哪些元素,并返回 这些元素的迭代器。传入 .. 范围语法可以移除 vector 中的每一个 值。

因此,我们需要像这样更新 ThreadPool 的 drop 实现:

文件名:src/lib.rs
#![allow(unused)]
fn main() {
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool { workers, sender }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
}

这样就解决了编译器错误,而且不需要对代码做任何其他修改。 需要注意的是,因为 drop 在 panic 时也可能被调用,所以 unwrap 也可能 panic,从而导致二次 panic,这会立即让程序 崩溃并终止任何正在进行的清理工作。对于示例程序来说这没有问题, 但不推荐在生产代码中这样做。

向线程发送停止监听任务的信号

经过我们所做的所有修改,代码编译时已经没有任何警告了。 然而坏消息是,这段代码还不能按我们期望的方式工作。 关键在于 Worker 实例的线程中运行的闭包里的逻辑: 目前我们调用了 join,但这并不会关闭线程, 因为它们会一直 loop 循环寻找任务。如果我们试图用当前的 drop 实现来丢弃 ThreadPool,主线程将会永远 阻塞,等待第一个线程结束。

为了解决这个问题,我们需要修改 ThreadPool 的 drop 实现,然后再修改 Worker 的循环。

首先,我们将修改 ThreadPool 的 drop 实现,在等待线程结束 之前显式地丢弃 sender。示例 21-23 展示了 对 ThreadPool 所做的显式丢弃 sender 的修改。与线程不同, 这里我们确实需要使用 Option,以便能够用 Option::take 将 sender 从 ThreadPool 中移出。

文件名:src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}
// --snip--

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        // --snip--

        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let job = receiver.lock().unwrap().recv().unwrap();

                println!("Worker {id} got a job; executing.");

                job();
            }
        });

        Worker { id, thread }
    }
}
示例 21-23:在 join Worker 线程之前显式丢弃 sender

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

文件名:src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in self.workers.drain(..) {
            println!("Shutting down worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let message = receiver.lock().unwrap().recv();

                match message {
                    Ok(job) => {
                        println!("Worker {id} got a job; executing.");

                        job();
                    }
                    Err(_) => {
                        println!("Worker {id} disconnected; shutting down.");
                        break;
                    }
                }
            }
        });

        Worker { id, thread }
    }
}
示例 21-24:当 recv 返回错误时显式跳出循环

为了看到这段代码的实际效果,让我们像示例 21-25 那样修改 main, 使服务器在优雅地关闭之前只接受两个请求。

文件名:src/main.rs
use hello::ThreadPool;
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
示例 21-25:在处理两个请求后退出循环以关闭服务器

你肯定不会希望一个真实世界的 Web 服务器只处理两个请求就关闭。 这段代码只是用来演示优雅停机和清理流程是正常工作的。

take 方法定义在 Iterator trait 中,它将迭代限制为最多前两项。 ThreadPool 会在 main 结束时离开作用域,随后 drop 实现就会运行。

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

$ 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 线程结束。

注意这次特定执行中一个有趣的细节:ThreadPool 丢弃了 sender,而在任何 Worker 收到错误之前,我们就尝试 join Worker 0。Worker 0 还没有从 recv 得到错误,所以主 线程阻塞了,等待 Worker 0 结束。与此同时,Worker 3 收到了一个任务,然后所有线程都收到了错误。当 Worker 0 结束时, 主线程继续等待其余的 Worker 实例结束。在那时, 它们都已经退出了自己的循环并停止了。

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

以下是完整代码,供参考:

文件名:src/main.rs
use hello::ThreadPool;
use std::{
    fs,
    io::{BufReader, prelude::*},
    net::{TcpListener, TcpStream},
    thread,
    time::Duration,
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }

    println!("Shutting down.");
}

fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&stream);
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
        "GET /sleep HTTP/1.1" => {
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();

    let response =
        format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    stream.write_all(response.as_bytes()).unwrap();
}
文件名:src/lib.rs
use std::{
    sync::{Arc, Mutex, mpsc},
    thread,
};

pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

type Job = Box<dyn FnOnce() + Send + 'static>;

impl ThreadPool {
    /// Create a new ThreadPool.
    ///
    /// The size is the number of threads in the pool.
    ///
    /// # Panics
    ///
    /// The `new` function will panic if the size is zero.
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let (sender, receiver) = mpsc::channel();

        let receiver = Arc::new(Mutex::new(receiver));

        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&receiver)));
        }

        ThreadPool {
            workers,
            sender: Some(sender),
        }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || {
            loop {
                let message = receiver.lock().unwrap().recv();

                match message {
                    Ok(job) => {
                        println!("Worker {id} got a job; executing.");

                        job();
                    }
                    Err(_) => {
                        println!("Worker {id} disconnected; shutting down.");
                        break;
                    }
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

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

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

总结

干得漂亮!你已经读到了本书的结尾!我们要感谢你 与我们一起完成了这次 Rust 之旅。现在你已经准备好实现自己的 Rust 项目,并为他人的项目提供帮助了。请记住,这里有一个 热情友好的 Rustacean 社区,他们很乐意帮助你应对 Rust 之旅中遇到的任何挑战。