홈강좌Rust 입문·중급 47편 중 43편째
Rust 스레드 — thread::spawn 으로 일을 나누고 move 로 넘긴다 (E0373)
Rust 의 thread::spawn 은 클로저(36편)를 새 스레드에서 돌리고, join 으로 끝나기를 기다려 결과를 받는다. 스레드는 만든 함수보다 오래 살 수 있어서, 바깥 변수를 빌리기만 하면 E0373 이 난다. move 로 변수를 스레드 안으로 옮기면 된다.
01 spawn 으로 띄우고 join 으로 기다린다
1 부터 1000 까지 더하는 일을 다른 스레드에 맡겨 보자.
use std::thread;
fn main() {
let handle = thread::spawn(|| {
let mut sum = 0;
for n in 1..=1000 {
sum += n;
}
sum
});
println!("main keeps going");
let sum = handle.join().unwrap();
println!("sum from thread: {sum}");
} Compiling threads v0.1.0 (~/projects/threads)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/threads`
main keeps going
sum from thread: 500500thread::spawn(|| { … })은 클로저를 새 스레드에서 돌린다.main은 기다리지 않고 바로 다음 줄로 간다. 그래서main keeps going이 먼저 찍혔다.spawn은 손잡이(JoinHandle)를 돌려준다.handle.join()은 그 스레드가 끝날 때까지 기다리고, 클로저의 반환값을Result로 준다(스레드가 패닉했으면Err). 여기서는unwrap으로 꺼냈다.sum += n은sum = sum + n의 줄임이다.
스레드는 운영체제가 동시에(코어가 여럿이면 정말로 같은 시각에) 돌리는 실행 흐름이다. 오래 걸리는 계산을 나눠 맡기면 전체 시간이 준다.
02 바깥 변수를 빌리면 E0373
스레드에서 main 의 목록을 찍어 보자.
use std::thread;
fn main() {
let names = vec!["ferris", "corro"];
let handle = thread::spawn(|| {
println!("{names:?}");
});
handle.join().unwrap();
}error[E0373]: closure may outlive the current function, but it borrows `names`, which is owned by the current function
--> src/main.rs:5:32
|
5 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `names`
6 | println!("{names:?}");
| ----- `names` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:5:18
|
5 | let handle = thread::spawn(|| {
| __________________^
6 | | println!("{names:?}");
7 | | });
| |______^
help: to force the closure to take ownership of `names` (and any other referenced variables), use the `move` keyword
|
5 | let handle = thread::spawn(move || {
| ++++- 무엇 — 클로저가 지금 함수보다 오래 살 수 있는데(may outlive),
main이 가진names를 빌린다. - 어디 — 5번째 줄의
||, 그리고 6번째 줄에서names를 빌렸다.note:는spawn이 넘겨받는 것이'static(35편, 프로그램 끝까지 살 수 있는 것)이어야 한다고 한다. - 고치는 법 —
help:가 클로저 앞에move를 붙여names의 주인을 클로저로 옮기라고 한다.
컴파일러는 이 스레드가 언제 끝날지 모른다. main 이 먼저 끝나 names 가 치워진 뒤에도 스레드가 그것을 읽으려 할 수 있다. 그래서 빌리는 것을 막는다.
03 move 로 넘긴다
use std::thread;
fn main() {
let names = vec!["ferris", "corro"];
let handle = thread::spawn(move || {
println!("{names:?}");
names.len()
});
let count = handle.join().unwrap();
println!("count: {count}");
} Compiling threads v0.1.0 (~/projects/threads)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
Running `target/debug/threads`
["ferris", "corro"]
count: 2move || 는 클로저가 쓰는 바깥 변수를 빌리지 않고 안으로 옮긴다(15편의 이동). 이제 names 의 주인은 스레드다. 스레드가 끝날 때 치워지니 문제가 없다. 대신 main 에서는 더 이상 names 를 쓸 수 없다. 필요하면 스레드가 결과를 돌려주게 한다(names.len()).
04 실습 — 범위를 나눠 스레드 셋으로 더하기
1~300 의 합을 세 구간으로 나눠, 스레드마다 한 구간씩 더하게 하자.
use std::thread;
fn main() {
let ranges = [(1, 100), (101, 200), (201, 300)];
let mut handles = Vec::new();
for (start, end) in ranges {
handles.push(thread::spawn(move || {
let mut sum = 0;
for n in start..=end {
sum += n;
}
sum
}));
}
let mut total = 0;
for h in handles {
total += h.join().unwrap();
}
println!("total: {total}");
} Compiling threads v0.1.0 (~/projects/threads)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/threads`
total: 45150for로 스레드 셋을 띄우고, 손잡이를Vec에 모은다. 구간의start·end는 정수(Copy)라move로 복사돼 들어간다.- 모두 띄운 뒤에 차례로
join해서 결과를 더한다. 띄우자마자join하면 하나가 끝나야 다음을 띄우니 나눈 의미가 없다.
1 부터 300 까지의 합 45150 이 나왔다. 세 스레드가 끝나는 순서는 실행할 때마다 다를 수 있지만, 결과는 손잡이 순서대로 모으니 늘 같다. 스레드끼리 값을 주고받는 다른 방법은 다음 편의 채널이다.