이 시리즈는 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/sleep
과 127.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 모델 등
다양한 모델이 존재하지만 우리는 쓰레드 풀에 대한 것만 다루도록 하겠다.
쓰레드 풀은 작업을 처리하기 위해 미리 생성되어 대기하고 있는 쓰레드의 그룹이다.
그렇게 대기하고 있다가 작업이 주어지면 그 중 하나가 나서서 그것을 처리하고 돌아온다.
처리하고 있는 도중에 다른 작업이 주어지면 다른 쓰레드가 그것을 처리하고 돌아온다.
이로써 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
중 하나를 통해 사용할 수 있는데
우리의 execute
는 thread::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_capacity
는 Vec::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
의 구현은 다음과 같이 이루어진다.
id
와 JoinHandle<()>
을 갖는 Worker
구조체를 정의한다.ThreadPool
이 Worker
인스턴스 객체를 갖도록 변경한다.id
를 받아 id
와 빈 클로저와 함께 생성된 쓰레드를 갖는 Worker
인스턴스를 반환하는 Worker::new
함수를 정의한다.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
를 생성하며 빈 클로저가 전달되기 때문이다.
따라서 Worker
가 ThreadPool
이 가진 작업 큐에서 클로저를 가져오는 기능이 있어야 한다.
채널을 통해 이 기능을 구현하도록 하겠다.
우리는 쓰레드 간의 통신에 대해 이야기하며 채널에 대해 배운 바가 있다.
수로에 물 흘려 보내듯이 데이터를 전달하는 이 방식은
작업 큐로부터 쓰레드로 작업을 전달하기에 적절하다.
그 과정은 다음과 같이 이루어진다.
ThreadPool
은 채널을 만들어 송신 측을 가지고 있는다.Worker
는 채널의 수신 측을 가지고 있는다.Job
구조체를 생성한다.execute
메서드는 채널의 송신 측을 통해 실행하고자 하는 작업을 전송한다.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>>
에 대해 배운 바 있다.
이것을 사용하면 여러 소유자가 동시 소유권을 가지면서 상호 배제를 보장할 수 있다.
우리가 작성한 코드에서 receiver
에 Arc<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에 해당합니다.