hyunsu blog

홈강좌Rust 입문·중급 47편 중 43편째

Rust 스레드 — thread::spawn 으로 일을 나누고 move 로 넘긴다 (E0373)

목차 · 4
  1. 01spawn 으로 띄우고 join 으로 기다린다
  2. 02바깥 변수를 빌리면 E0373
  3. 03move 로 넘긴다
  4. 04실습 — 범위를 나눠 스레드 셋으로 더하기

Rust 의 thread::spawn 은 클로저(36편)를 새 스레드에서 돌리고, join 으로 끝나기를 기다려 결과를 받는다. 스레드는 만든 함수보다 오래 살 수 있어서, 바깥 변수를 빌리기만 하면 E0373 이 난다. move 로 변수를 스레드 안으로 옮기면 된다.

01 spawn 으로 띄우고 join 으로 기다린다

1 부터 1000 까지 더하는 일을 다른 스레드에 맡겨 보자.

rust
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}");
}
text
   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: 500500
  • thread::spawn(|| { … }) 은 클로저를 새 스레드에서 돌린다. main 은 기다리지 않고 바로 다음 줄로 간다. 그래서 main keeps going 이 먼저 찍혔다.
  • spawn 은 손잡이(JoinHandle)를 돌려준다. handle.join() 은 그 스레드가 끝날 때까지 기다리고, 클로저의 반환값을 Result 로 준다(스레드가 패닉했으면 Err). 여기서는 unwrap 으로 꺼냈다.
  • sum += n 은 sum = sum + n 의 줄임이다.

스레드는 운영체제가 동시에(코어가 여럿이면 정말로 같은 시각에) 돌리는 실행 흐름이다. 오래 걸리는 계산을 나눠 맡기면 전체 시간이 준다.

02 바깥 변수를 빌리면 E0373

스레드에서 main 의 목록을 찍어 보자.

rust
use std::thread;

fn main() {
    let names = vec!["ferris", "corro"];
    let handle = thread::spawn(|| {
        println!("{names:?}");
    });
    handle.join().unwrap();
}
text
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 || {
  |                                ++++
  1. 무엇 — 클로저가 지금 함수보다 오래 살 수 있는데(may outlive), main 이 가진 names 를 빌린다.
  2. 어디 — 5번째 줄의 ||, 그리고 6번째 줄에서 names 를 빌렸다. note: 는 spawn 이 넘겨받는 것이 'static(35편, 프로그램 끝까지 살 수 있는 것)이어야 한다고 한다.
  3. 고치는 법 — help: 가 클로저 앞에 move 를 붙여 names 의 주인을 클로저로 옮기라고 한다.

컴파일러는 이 스레드가 언제 끝날지 모른다. main 이 먼저 끝나 names 가 치워진 뒤에도 스레드가 그것을 읽으려 할 수 있다. 그래서 빌리는 것을 막는다.

03 move 로 넘긴다

rust
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}");
}
text
   Compiling threads v0.1.0 (~/projects/threads)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/threads`
["ferris", "corro"]
count: 2

move || 는 클로저가 쓰는 바깥 변수를 빌리지 않고 안으로 옮긴다(15편의 이동). 이제 names 의 주인은 스레드다. 스레드가 끝날 때 치워지니 문제가 없다. 대신 main 에서는 더 이상 names 를 쓸 수 없다. 필요하면 스레드가 결과를 돌려주게 한다(names.len()).

04 실습 — 범위를 나눠 스레드 셋으로 더하기

1~300 의 합을 세 구간으로 나눠, 스레드마다 한 구간씩 더하게 하자.

rust
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}");
}
text
   Compiling threads v0.1.0 (~/projects/threads)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/threads`
total: 45150
  • for 로 스레드 셋을 띄우고, 손잡이를 Vec 에 모은다. 구간의 start·end 는 정수(Copy)라 move 로 복사돼 들어간다.
  • 모두 띄운 뒤에 차례로 join 해서 결과를 더한다. 띄우자마자 join 하면 하나가 끝나야 다음을 띄우니 나눈 의미가 없다.

1 부터 300 까지의 합 45150 이 나왔다. 세 스레드가 끝나는 순서는 실행할 때마다 다를 수 있지만, 결과는 손잡이 순서대로 모으니 늘 같다. 스레드끼리 값을 주고받는 다른 방법은 다음 편의 채널이다.