やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Rustのサーバ(シャットダウン)

 優美かどうかはわからない。

成果物

参考

ThreadPoolDropトレイトを実装する

src/lib.rs

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            worker.thread.join().unwrap();
        }
    }
}
$ cargo run
...
error[E0507]: cannot move out of borrowed content
  --> src/lib.rs:53:13
   |
53 |             worker.thread.join().unwrap();
   |             ^^^^^^^^^^^^^ cannot move out of borrowed content

 「借用したものから所有権を移動できません」と怒られる。join()threadの所有権を奪う。だが、親のworkerは借用されたものなので所有権を奪えず実行できない。

Workerが代わりにOption<thread::JoinHandle<()>>を保持していれば、 Optionに対してtakeメソッドを呼び出し、Some列挙子から値をムーブし、その場所にNone列挙子を残すことができます。言い換えれば、実行中のWorkerにはthreadにSome列挙子があり、Workerを片付けたい時には、 ワーカーが実行するスレッドがないようにSomeをNoneで置き換えるのです。

struct Worker {
    id: usize,
    thread: Option<thread::JoinHandle<()>>,
}
$ cargo run
...
error[E0599]: no method named `join` found for type `std::option::Option<std::thread::JoinHandle<()>>` in the current scope
  --> src/lib.rs:53:27
   |
53 |             worker.thread.join().unwrap();
   |                           ^^^^

error[E0308]: mismatched types
  --> src/lib.rs:72:13
   |
72 |             thread,
   |             ^^^^^^
   |             |
   |             expected enum `std::option::Option`, found struct `std::thread::JoinHandle`
   |             help: try using a variant of the expected type: `Some(thread)`
   |
   = note: expected type `std::option::Option<std::thread::JoinHandle<()>>`
              found type `std::thread::JoinHandle<_>`
impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // --snip--
        Worker {
            id,
            thread: Some(thread),
        }
impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

 Option.take()は実行後にSomeをとりだしたあとNoneをセットする。

スレッドに仕事をリッスンするのを止めるよう通知する

 現状、仕事を求めて無限にloopする。これを修正する。

enum Message {
    NewJob(Job),
    Terminate,
}
pub struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}
// --snip--
impl ThreadPool {
    // --snip--
    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {
        let job = Box::new(f);
        self.sender.send(Message::NewJob(job)).unwrap();
    }
}
// --snip--
impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) -> Worker {
        let thread = thread::spawn(move ||{
            loop {
                let message = receiver.lock().unwrap().recv().unwrap();
                match message {
                    Message::NewJob(job) => {
                        println!("Worker {} got a job; executing.", id);
                        job.call_box();
                    },
                    Message::Terminate => {
                        // ワーカー{}は停止するよう指示された
                        println!("Worker {} was told to terminate.", id);
                        break;
                    },
                }
            }
        });
        Worker {
            id,
            thread: Some(thread),
        }
    }
}

 ポイントはWorker.new()クロージャMessage::Terminateのときloopからbreakする。

 次に、DropTerminateメッセージを送信する。Terminateメッセージ送信とjoin呼出がそれぞれ別々のループである必要がある。さもなくばデッドロックする。

impl Drop for ThreadPool {
    fn drop(&mut self) {
        println!("Sending terminate message to all workers.");
        for _ in &mut self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }
        println!("Shutting down all workers.");
        for worker in &mut self.workers {
            println!("Shutting down worker {}", worker.id);
            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

 終了するか動作確認するため、2回リクエストされたら終了するコードを書いてみる。

src/main.rs

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.");
}
$ cargo run
   Compiling hello v0.1.0 (/tmp/work/Rust.HttpServer.Shutdown.20190710090353/src/4/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 3.96s
     Running `target/debug/hello`
Worker 1 got a job; executing.
Shutting down.
Sending terminate message to all workers.
Worker 0 got a job; executing.
Shutting down all workers.
Shutting down worker 0
Worker 2 was told to terminate.
Worker 3 was told to terminate.
Worker 1 was told to terminate.
Worker 0 was told to terminate.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3

 Iterator.take(n)は最大でも繰り返し数をn回にする。

 以上。完成。

所感

 え、2回目のリクエストがきたら終了するのが「優美なシャットダウン」なの? と思ってしまう。

対象環境

  • Raspbierry pi 3 Model B+
  • Raspbian stretch 9.0 2018-11-13
  • bash 4.4.12(1)-release

  • bash 4.4.12(1)-release

  • rustc 1.34.2 (6c2484dc3 2019-05-13)
  • cargo 1.34.0 (6789d8a0a 2019-04-01)
$ uname -a
Linux raspberrypi 4.19.42-v7+ #1219 SMP Tue May 14 21:20:58 BST 2019 armv7l GNU/Linux

前回まで