#37 최종 프로젝트! 웹서버 만들기 中 멀티 쓰레드 웹서버

Pt J·2020년 9월 29일
1

[完] Rust Programming

목록 보기
40/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

지난 시간에 작성하던 예제에서 이어서 이야기해보자.

우리 서버는 요청이 들어오면 그것을 순서대로 처리한다.
하나의 쓰레드에서 모든 일처리를 하기 때문에 요청에 대한 응답이 선형적으로 이루어지는 것이다.
따라서 처리하는 데 오래 걸리는 요청이 들어올 경우 그것 하나 처리하는 것 때문에
원래라면 금방 처리되고 응답 받을 요청들이 한참을 대기하게 될 수 있다.

이를 확인하기 위해 처리하는 데 오래 걸리는 요청을 하나 가정해보겠다.
URI가 /sleep이라면 5초 정지하도록 하고, 이것을 오래 걸리는 요청으로 가정한다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/main.rs

src/main.rs

use std::thread;
use std::time::Duration;

// snip

fn handle_connection(mut stream: TcpStream) {
    // snip

    let sleep = b"GET /sleep HTTP/1.1\r\n";

    let (status_line, filename) = if buffer.starts_with(get) {
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        ("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
    } else {
        ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
    };

    // snip
}

그리고 cargo run으로 프로그램을 실행시킨 후
두 개의 브라우저를 열어 127.0.0.1:7878/sleep127.0.0.1:7878로 접속해보자.

순서대로 요청할 경우
127.0.0.1:7878/sleep뿐만 아니라 127.0.0.1:7878도 지연되는 것을 확인할 수 있다.
반대로 요청할 경우 127.0.0.1:7878는 바로 뜨지만 127.0.0.1:7878/sleep된다.
이는 127.0.0.1:7878/sleep이 먼저 요청되었을 때 그것이 다 처리될 때까지
127.0.0.1:7878는 뒤에서 대기하고 있기 때문이다.

대부분의 웹 서버는 이러한 이슈를 해결하기 위한 다양한 우회 방법을 사용하는데
그 중 하나인 쓰레드 풀에 대해 알아보도록 하자.

이 외에도 요청이 들어올 때마다 fork하고 처리 후 join하는 fork/join 모델과
하나의 쓰레드를 비동기적으로 사용하는 single-threaded async I/O 모델 등
다양한 모델이 존재하지만 우리는 쓰레드 풀에 대한 것만 다루도록 하겠다.

쓰레드 풀 Thread Pool

쓰레드 풀은 작업을 처리하기 위해 미리 생성되어 대기하고 있는 쓰레드의 그룹이다.
그렇게 대기하고 있다가 작업이 주어지면 그 중 하나가 나서서 그것을 처리하고 돌아온다.
처리하고 있는 도중에 다른 작업이 주어지면 다른 쓰레드가 그것을 처리하고 돌아온다.
이로써 N개의 쓰레드를 가진 쓰레드 풀은 최대 N개의 작업을 동시에 처리할 수 있으며
빨리 끝나면 빨리 끝나는대로 다음 작업을 처리할 수 있다.

경우에 따라서는 쓰레드를 무한히 만들어낼 수 있도록 할 수도 있겠지만
그렇게 하면 모든 쓰레드에 엄청난 요청을 보내는 서비스 거부 공격에 취약해지므로
정해진 개수의 쓰레드만 사용하도록 하겠다.

지난 번에 테스트 주도 개발 방법을 사용했던 것과 유사하게
컴파일러 주도 개발 방법을 통해 쓰레드 풀을 구현하도록 하자.

컴파일러 주도 개발 방법
우리가 원하는 함수를 호출하는 코드를 작성하고 그것을 작동시키기 위해서 무엇을 수정해야 하는지 컴파일러로부터 받은 오류를 보고 판단한다.

요청 시마다 쓰레드 생성

요청을 받을 때마다 쓰레드를 생성하는 방식으로 코드를 작성한다고 하자.
이를 위해서는 다음과 같이 main에서 요청을 받을 때마다 쓰레드를 생성해
생성된 쓰레드에서 handle_connection 함수를 호출하도록 작성하면 된다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/main.rs

src/main.rs

// snip

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

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

        thread::spawn(|| {
            handle_connection(stream);
        });
    }
}

// snip

물론 우리는 서비스 거부 공격에 대비해 쓰레드의 개수를 제한하기로 하였으니
이 방식으로 작성하지는 않을 것이다.

쓰레드 수 제한

제한된 개수의 쓰레드를 관리하는 쓰레드 풀을 만들어 쓰레드를 관리하도록 하자.
매번 thread::spawn 함수를 호출하는 게 아니라
가상의 ThreadPool 구조체를 사용하도록 하겠다.
물론 이 녀석은 아직 구현되지 않았기 때문에 오류가 발생하겠지만
우리는 그러한 오류를 보며 컴파일러 주도 개발 방법을 사용하기로 했으니 이를 작성해보자.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/main.rs

src/main.rs

// snip

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

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

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

// snip

빌드 가능한지 체크해보면 당연히 다음과 같은 오류를 확인할 수 있다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
error[E0433]: failed to resolve: use of undeclared type or module `ThreadPool`
  --> src/main.rs:10:16
   |
10 |     let pool = ThreadPool::new(4);
   |                ^^^^^^^^^^ use of undeclared type or module `ThreadPool`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter20/hello$

ThreadPool 작성

컴파일러가 띄워준 오류 메시지를 보며 ThreadPool 구조체를 조금씩 구현해보자.
아직 이 구조체에 무엇이 필요한지 알 수 없으니 빈 구조체로 선언하겠다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

pub struct ThreadPool;
peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/main.rs

src/main.rs

use hello::ThreadPool;
// snip

물론 구조체를 선언한 것만으로는 아직 컴파일이 되지 않는다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
error[E0599]: no function or associated item named `new` found for struct `hello::ThreadPool` in the current scope
  --> src/main.rs:11:28
   |
11 |     let pool = ThreadPool::new(4);
   |                            ^^^ function or associated item not found in `hello::ThreadPool`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter20/hello$

하지만 컴파일러의 오류 메시지는 우리에게 다음 작업으로 무엇이 필요한지 알려준다.
정수값을 인자로 받아 ThreadPool을 반환하는 new 함수가 필요하다.
개수는 음수일 수 없으므로 정수 자료형은 usize가 적당하다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }
}
peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: unused variable: `size`
 --> src/lib.rs:4:16
  |
4 |     pub fn new(size: usize) -> ThreadPool {
  |                ^^^^ help: if this is intentional, prefix it with an underscore: `_size`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: 1 warning emitted

error[E0599]: no method named `execute` found for struct `hello::ThreadPool` in the current scope
  --> src/main.rs:16:14
   |
16 |         pool.execute(|| {
   |              ^^^^^^^ method not found in `hello::ThreadPool`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter20/hello$ 

경고에 대해서는 이후에 ThreadPool에 필드를 추가하면서 해결하도록 하고
오류에 대한 처리를 해보도록 하자.
이것도 new와 마찬가지로, execute 메서드가 구현되어야 한다는 것이다.
execute는 매개변수로 클로저를 하나 가지고 있다.
클로저는 Fn, FnMut, FnOnce 중 하나를 통해 사용할 수 있는데
우리의 executethread::spawn과 유사한 기능을 하게 될테니
thread::spawn의 시그니처를 참고하도록 하겠다.

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T + Send + 'static,
        T: Send + 'static

thread::spawn은 클로저를 위한 제네릭의 트레이트 경계로
FnOnce를 사용하고 있으며 추가적으로 Send'static도 사용하고 있음을 알 수 있다.
Send는 클로저를 다른 쓰레드로 전달할 때 사용되고
'static은 쓰레드의 수명을 알 수 없기에 필요하므로
이 두 녀석도 그대로 사용하도록 하자.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

pub struct ThreadPool;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }

    pub fn execute<F>(&self, f: F)
        where
            F: FnOnce() + Send + 'static
    {
        // TODO implement
    }
}
peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: unused variable: `size`
 --> src/lib.rs:4:16
  |
4 |     pub fn new(size: usize) -> ThreadPool {
  |                ^^^^ help: if this is intentional, prefix it with an underscore: `_size`
  |
  = note: `#[warn(unused_variables)]` on by default

warning: unused variable: `f`
 --> src/lib.rs:8:30
  |
8 |     pub fn execute<F>(&self, f: F)
  |                              ^ help: if this is intentional, prefix it with an underscore: `_f`

warning: 2 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
peter@hp-laptop:~/rust-practice/chapter20/hello$ 

이제 경고만 뜬다.
기능과는 별개로 일단 컴파일은 가능하다는 것이다.

쓰레드 수 검증

우리는 new 함수의 매개변수 자료형을 usize로 설정함으로써
쓰레드 개수가 음수로 들어오는 것을 방지하였다.
그런데 usize는 여전히 0일 수 있다.
따라서 size는 0보다 커야 한다는 것을 강제하도록 하겠다.

이 때, 이 사실을 문서화 주석을 통해
이 함수에서 패닉이 발생할 수 있음을 명시한다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

pub struct ThreadPool;

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);

        ThreadPool
    }

    // snip
}

쓰레드 저장

ThreadPool은 size 개의 쓰레드를 저장해놓고 사용해야 한다.
쓰레드를 저장해놓고 사용하는 방법을 고민하며 thread::spawn의 시그니처를 다시 확인해보자.

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
    where
        F: FnOnce() -> T + Send + 'static,
        T: Send + 'static

이 함수는 JoinHandle<T>을 통해 쓰레드를 관리하는 것으로 보인다.
그런 의미에서 ThreadPool이 JoinHandle<T> 벡터를 관리하도록 작성해보자.
new 함수에서 size 개의 JoinHandle<T>를 가진 벡터를 생성하는 것이다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

use std::thread;

pub struct ThreadPool {
    threads: Vec<thread::JoinHandle<()>>,
}

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 mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            // TODO create some threads and store them in the vector
        }

        ThreadPool { threads }
    }

    // snip
}

Vec::with_capacityVec::new와 비슷하지만 그 공간을 미리 할당하여
크기가 정해져 있는 경우에 효율적이다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check    
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: unused variable: `f`
  --> src/lib.rs:27:30
   |
27 |     pub fn execute<F>(&self, f: F)
   |                              ^ help: if this is intentional, prefix it with an underscore: `_f`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: variable does not need to be mutable
  --> src/lib.rs:18:13
   |
18 |         let mut threads = Vec::with_capacity(size);
   |             ----^^^^^^^
   |             |
   |             help: remove this `mut`
   |
   = note: `#[warn(unused_mut)]` on by default

warning: field is never read: `threads`
 --> src/lib.rs:4:5
  |
4 |     threads: Vec<thread::JoinHandle<()>>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: 3 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
peter@hp-laptop:~/rust-practice/chapter20/hello

경고는 몇 개 뜨지만 컴파일 하는 데는 문제가 없다.

쓰레드에게 작업 전달

쓰레드를 생성하여 작업을 수행하도록 하기 위해서는
// TODO create some threads and store them in the vector 부분을 구현해야 한다.
그런데 표준 라이브러리의 쓰레드 생성 방법인 thread::spawn
쓰레드가 생성되자마자 그것이 실행할 코드를 클로저로 받아서 바로 실행하도록 하여
우리가 원하는 방식인 쓰레드를 미리 만들어놓고 나중에 실행시키는 방법과는 거리가 있다.

따라서 이를 위핸 새로운 자료구조 Worker를 생성하도록 하겠다.
ThreadPool은 JoinHandle<()> 벡터가 아니라 Worker 벡터를 가지며
Worker 하나 당 하나의 JoinHandle<()>을 갖도록 한다.
각각의 Worker는 구분을 위해 id를 가지며
실행 중인 쓰레드에 클로저를 전달하는 메서드를 구현한다.

Worker의 구현은 다음과 같이 이루어진다.

  1. idJoinHandle<()>을 갖는 Worker 구조체를 정의한다.
  2. ThreadPoolWorker 인스턴스 객체를 갖도록 변경한다.
  3. id를 받아 id와 빈 클로저와 함께 생성된 쓰레드를 갖는 Worker 인스턴스를 반환하는 Worker::new 함수를 정의한다.
  4. Thread::new에서 loop 카운터로 id를 생성하고 그 id를 통해 새 Worker를 생성하며 그것을 벡터에 저장하도록 한다.
peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

use std::thread;

pub struct ThreadPool {
    workers: Vec<Worker>,
}

impl ThreadPool {
    // snip

    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

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

        for id in 0..size {
            workers.push(Worker::new(id));
        }

        ThreadPool{ workers }
    }

    // snip
}

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

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

Worker 구조체는 ThreadPool에서만 사용할 뿐 외부에서는 알 필요 없으므로
pub 키워드를 붙이지 않았다.

그런데 우리가 구현한 코드는 빈 클로저를 전달받아 쓰레드에서 아무것도 하지 않는다.
사용할 땐 쓰레드에서 실행시킬 클로저는 execute 메서드를 통해 전달되는데
실제로는 ThreadPool을 생성할 때 Worker를 생성하며 빈 클로저가 전달되기 때문이다.

따라서 WorkerThreadPool이 가진 작업 큐에서 클로저를 가져오는 기능이 있어야 한다.
채널을 통해 이 기능을 구현하도록 하겠다.

채널을 통한 요청 전달

우리는 쓰레드 간의 통신에 대해 이야기하며 채널에 대해 배운 바가 있다.
수로에 물 흘려 보내듯이 데이터를 전달하는 이 방식은
작업 큐로부터 쓰레드로 작업을 전달하기에 적절하다.

그 과정은 다음과 같이 이루어진다.

  1. ThreadPool은 채널을 만들어 송신 측을 가지고 있는다.
  2. 각각의 Worker는 채널의 수신 측을 가지고 있는다.
  3. 채널을 통해 흘려 보낼 클로저를 가진 Job 구조체를 생성한다.
  4. execute 메서드는 채널의 송신 측을 통해 실행하고자 하는 작업을 전송한다.
  5. 쓰레드에서 Worker는 채널의 수신 측을 무한 루프를 돌며 수신되는 작업의 클로저를 실행한다.

이를 위해 새로운 Job 구조체가 필요하고
ThreadPool에는 채널의 송신자를, Worker에는 수신자를 추가해야 한다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

use std::thread;
use std::sync::mpsc;

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

impl ThreadPool {
    // snip
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

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

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

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

        ThreadPool{ workers, sender }
    }

    // snip
}

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

impl Worker {
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

struct Job;

이제 컴파일이 되는지 체크해보자.

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: unused variable: `f`
  --> src/lib.rs:31:30
   |
31 |     pub fn execute<F>(&self, f: F)
   |                              ^ help: if this is intentional, prefix it with an underscore: `_f`
   |
   = note: `#[warn(unused_variables)]` on by default

error[E0382]: use of moved value: `receiver`
  --> src/lib.rs:25:42
   |
20 |         let (sender, receiver) = mpsc::channel();
   |                      -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 |             workers.push(Worker::new(id, receiver));
   |                                          ^^^^^^^^ value moved here, in previous iteration of loop

error: aborting due to previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter20/hello$ 

컴파일이 되지 않는데 그 원인은 receiver의 중복 사용이다.
Rust의 채널은 Multiple Producer, Single Consumer로,
여러 물줄기가 모여 하나의 강이 될 수는 있지만
하나의 물줄기에서 시작되어 더 작은 물줄기로 쪼개지지는 않는다는 것을 배웠다.

채널 큐에서 작업을 꺼내오는 것은 수신자를 내부적으로 변경하므로
송신 측에서는 일르 안전하게 공유하고 변경할 수 있어야 한다.
그리고 우리는 마침 그것을 가능케 하는 Arc<Mutex<T>>에 대해 배운 바 있다.
이것을 사용하면 여러 소유자가 동시 소유권을 가지면서 상호 배제를 보장할 수 있다.

우리가 작성한 코드에서 receiverArc<Mutex<T>>를 적용해보자.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

use std::thread;
use std::sync::{mpsc, Arc, Mutex}
// snip

impl ThreadPool {
    // snip
    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 }
    }

    // snip
}

// snip

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

struct Job;

이제 다시 컴파일이 되는지 체크해보면,

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: unused variable: `f`
  --> src/lib.rs:31:30
   |
31 |     pub fn execute<F>(&self, f: F)
   |                              ^ help: if this is intentional, prefix it with an underscore: `_f`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: field is never read: `workers`
 --> src/lib.rs:4:5
  |
4 |     workers: Vec<Worker>,
  |     ^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: field is never read: `sender`
 --> src/lib.rs:5:5
  |
5 |     sender: mpsc::Sender<Job>,
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^

warning: field is never read: `id`
  --> src/lib.rs:40:5
   |
40 |     id: usize,
   |     ^^^^^^^^^

warning: field is never read: `thread`
  --> src/lib.rs:41:5
   |
41 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: path statement with no effect
  --> src/lib.rs:47:13
   |
47 |             receiver;
   |             ^^^^^^^^^
   |
   = note: `#[warn(path_statements)]` on by default

warning: 6 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
peter@hp-laptop:~/rust-practice/chapter20/hello$ 

경고는 몇 개 존재하지만 컴파일은 문제 없다.
이제 // TODO implement라고 적어놓고 미뤄두었던 execute 메서드를 구현해보자.

execute 구현

execute 메서드를 구현하기 앞서,
우리는 Job을 구조체로 선언해놓았지만
이것을 구조체가 아닌, 클로저를 저장할 트레이트 객체에 대한 별칭으로 변경하겠다.
그리고 execute에서는 Job 인스턴스를 생성해 채널을 통해 전송하도록 하겠다.
Worker::new에서는 채널의 수신자를 무한히 확인하며 작업이 전달되도록 코드를 수정한다.

peter@hp-laptop:~/rust-practice/chapter20/hello$ vi src/lib.rs

src/lib.rs

// snip

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

impl ThreadPool {
    // snip

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

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

// snip

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 {} got a job; executing.", id);

                job();
            }
        );

        Worker { id, thread }
    }
}
peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo check
    Checking hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: field is never read: `workers`
 --> src/lib.rs:4:5
  |
4 |     workers: Vec<Worker>,
  |     ^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: field is never read: `id`
  --> src/lib.rs:44:5
   |
44 |     id: usize,
   |     ^^^^^^^^^

warning: field is never read: `thread`
  --> src/lib.rs:45:5
   |
45 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: 3 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
peter@hp-laptop:~/rust-practice/chapter20/hello$ 

세 개의 경고가 남아있긴 하지만 실행하는 데는 문제가 없을 것이다.
이제 컴파일을 하고 다시 브라우저로 요청해보자.

peter@hp-laptop:~/rust-practice/chapter20/hello$ cargo run
   Compiling hello v0.1.0 (/home/peter/rust-practice/chapter20/hello)
warning: field is never read: `workers`
 --> src/lib.rs:4:5
  |
4 |     workers: Vec<Worker>,
  |     ^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(dead_code)]` on by default

warning: field is never read: `id`
  --> src/lib.rs:44:5
   |
44 |     id: usize,
   |     ^^^^^^^^^

warning: field is never read: `thread`
  --> src/lib.rs:45:5
   |
45 |     thread: thread::JoinHandle<()>,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: 3 warnings emitted

    Finished dev [unoptimized + debuginfo] target(s) in 0.94s
     Running `target/debug/hello`


이제는 http://127.0.0.1:7878/sleep를 요청하고 응답을 받기 전에
http://127.0.0.1:7878를 요청하면
기다리지 않고 바로 뜨는 것을 확인할 수 있다.

그리고 그러는 동안 터미널에는 다음과 같이 메시지가 출력된다.

Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 2 got a job; executing.
Worker 0 got a job; executing.
Worker 1 got a job; executing.

우리는 두 시간에 걸쳐 단일 쓰레드로 웹 서버를 만들고
오래 걸리는 작업에 대한 이슈를 해결하기 위해 쓰레드 풀을 이용한 방식으로 변경해보았다.
다음 시간에는 이 쓰레드 풀 웹 서버를 조금 더 개선하는 작업을 하고
Rust 프로그래밍의 기나긴 여정을 마무리짓도록 하겠다.

이 포스트의 내용은 공식문서의 20장 2절 Turning Our Single-Threaded Server into a Multithreaded Server에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글