2. 스레드 생성하기
먼저 스레드의 관점에서 main 함수를 살펴보겠습니다.
fn main(){
println!("hello!");
}
Rust는 main 함수에서 프로그램이 시작되고 끝납니다.
main 함수를 실행하는 일도 하나의 스레드입니다.
main 함수의 실행이 끝나고 스레드가 종료되면 프로그램이 종료됩니다.
이 점이 main 스레드의 특별한 점입니다.
main 스레드 위에서 다른 스레드가 만들어져서 실행되면 그 스레드는 main 스레드의 자식 스레드가 됩니다.
자식 스레드가 끝나지 않아도 main 스레드가 끝나면 프로그램은 종료됩니다.
결과적으로 자식 스레드는 제대로 실행되지 않거나 실행될 기회를 잡지 못할 수도 있습니다.
스레드를 만들어서 과연 그런지 시험해 보겠습니다.
새로운 스레드를 만들 때에는 std::thread::spawn 메서드를 사용합니다.
use std::thread;
fn main() {
thread::spawn(||{ // 1
println!("child thread");
"Hello World!”.to_string(). // 2
});
println!("main thread");
}
// 출력
main thread
// 1: 스레드 생성
출력을 보면, 기대와는 다르게 mian 스레드에 속한 println!("main thread"); 만 실행되었습니다.
새로 만든 스레드는 실행되지 않았습니다.
이것은 새로 만든 스레드가 실행되기도 전에 프로그램이 “main thread”를 출력하고 끝나버렸기 때문입니다.
main 스레드가 끝나면 프로그램도 종료됩니다.
새로 만든 스레드가 실행되도록 만들기 위해 thread::spawn 메소드를 살펴보겠습니다.
thread::spawn 메서드의 함수 시그니처는 다음과 같습니다.
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
반환 타입으로 JoinHandle을 사용합니다.
Rust 도큐먼트에 따르면 JoinHandle은 새로 만든 스레드를 붙이는 역할을 합니다.
스레드를 붙인다는 것은 main 스레드를 새로 만든 스레드가 끝날 때까지 기다리게 한다는 의미입니다.
main 스레드의 타임라인이 한 줄로 놓여 있는데 거기에 자식 스레드들의 가지를 여기저기 붙여서 main 스레드의 타임라인 안에 반드시 포함되게 하는 이미지입니다.
// 2: 우리가 만든 스레드에서는 클로저의 반환 값인 "Hello World!”.to_string()이 JoinHandle 입니다.
더 정확히 말하자면 클로저의 반환값을 thread::spawn이 받아서 다시 반환 값으로 사용합니다.
이때 타입이 JoinHandle<T>입니다. T는 당연히 클로저의 반환값 타입입니다.
우리는 thread::spawn의 파라미터로 클로저를 만들어서 사용했는데, 클로저 안에 우리가 실행하기 원하는 코드를 만들어 넣을 수 있습니다.
| | {. . .}
이 형식은 클로저입니다. 기억나지 않으시면 클로저 부분을 다시 보십시오.
스레드에서는 대부분 클로저로 실행 내용을 정의하기 때문에 클로저가 중요합니다.
위 예에서는 클로저의 반환 값"Hello World!”.tostring()이 JoinHandle에 담겨서 thread::spawn의 반환값이 됩니다.
그래서 String 타입인 "Hello World!”가 JoinHandle<T>의 T가 됩니다.
이제 JoinHandle을 사용해서 새로 만든 스레드가 실행되도록 해 보겠습니다.
use std::thread;
fn main() {
let thread1 = thread::spawn(||{
println!("child thread");
"Hello World!".to_string() // JoinHandle
});
println!("main thread");
println!("{}", thread1.join().unwrap()); // 1
}
// 출력
main thread
child thread
Hello World!
새로 만든 스레드를 thread1 변수에 담았습니다.
thread1 변수는 JoinHandle<String> 타입입니다.
// 1: join 메서드로 스레드가 실행되도록 기다리게 했습니다.
Join 메소드는
pub fn join(self) -> Result<T>
시그니처를 가집니다. 즉 반환값이 Result입니다. Result enum은 Ok 아니면 Err를 반환합니다. Ok 일 때, 값만 추출해서 보기 위해 unwrap을 사용했습니다.
출력을 보면 main 스레드가 가장 먼저 실행되고 다음은 스레드의 클로저가 실행되었습니다.
그리고 마지막으로 JoinHandle이 출력되었습니다.
JoinHandle 출력을 강조해서 보기 위해 다음과 같이 수정해서 출력해 보았습니다. (수정한 부분만 표시했습니다)
println!("{:#?}", thread1.join());
// 출력
Ok(
"Hello World!",
)
Result 타입의 Ok가 JoinHandle인 "Hello World!”를 가지고 있습니다.
스레드를 만드는 방법이 spawn 메서드를 사용 하는 방법만 있는 것은 아닙니다. 다음 코드를 보겠습니다.
use std::thread;
fn main() {
thread::Builder::new() // 1
.name("my_thread".to_string())
.spawn(|| println!("from my_thread"))
.unwrap()
.join().expect("fail!!!");
}
// 출력
from my_thread
// 1: thread::Builder::new() 메서드를 사용 해서 새로운 스레드를 생성한 후에 메소드 체이닝을 통해서 특성을 더해 나가는 형식입니다. ( 메소드 체이닝이란 “.메소드1.메소드2.메소드3”처럼 “.”으로 메소드를 계속 연결하는 방식을 말합니다.)
이때 각 메서드가 어떤 타입의 메서드이고 또 어떤 타입을 반환하는지 고려해서 순서를 정해야 합니다.
참고로, name, spawn은 Builder의 메서드입니다. unwrap은 spawn 메소드의 반환값 타입인 Result에 구현 된 메소드입니다. join은 JoinHandle 타입에 구현되어 있습니다.
그래서 체이닝의 순서를 바꿔도 상관없는 경우가 있는가 하면 순서를 바꾸면 원하는 결과를 얻지 못하거나 에러가 나는 경우도 있습니다.
'Rust 입문' 카테고리의 다른 글
[Rust] Message passing ( channel로 다른 스레드의 값 사용 하기 ) (0) | 2022.10.27 |
---|---|
[Rust] 스레드에서 외부값 사용하기 (0) | 2022.10.26 |
[Rust] 동시성 (0) | 2022.10.24 |
[Rust] RefCell 그리고 Arc, Mutex (0) | 2022.10.23 |
[Rust] Cell (0) | 2022.10.22 |