hyunsu blog

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

Rust Arc 와 Mutex — 여러 스레드가 한 값을 함께 고친다

목차 · 3
  1. 01Rc 는 스레드로 보낼 수 없다
  2. 02Mutex — 한 번에 한 스레드만
  3. 03실습 — 스레드 넷이 함께 세기

Rust 에서 여러 스레드가 한 값을 함께 가지려면 Arc 를 쓴다. Rc 를 스레드에 넘기면 E0277 로 막힌다. 함께 고치려면 Mutex 로 감싸서, lock() 으로 한 번에 한 스레드만 들어가게 한다. Arc<Mutex<T>> 로 스레드 넷이 1000번씩 더하면 언제나 4000 이 나온다.

01 Rc 는 스레드로 보낼 수 없다

39편의 Rc 로 값을 나눠 스레드에 넘겨 보자.

rust
use std::rc::Rc;
use std::thread;

fn main() {
    let shared = Rc::new(5);
    let copy = Rc::clone(&shared);
    let handle = thread::spawn(move || {
        println!("{copy}");
    });
    handle.join().unwrap();
}
text
error[E0277]: `Rc<i32>` cannot be sent between threads safely
 --> src/main.rs:7:32
  |
7 |       let handle = thread::spawn(move || {
  |                    ------------- ^------
  |                    |             |
  |  __________________|_____________within this `{closure@src/main.rs:7:32: 7:39}`
  | |                  |
  | |                  required by a bound introduced by this call
8 | |         println!("{copy}");
9 | |     });
  | |_____^ `Rc<i32>` cannot be sent between threads safely
  |
  = help: within `{closure@src/main.rs:7:32: 7:39}`, the trait `Send` is not implemented for `Rc<i32>`
  1. 무엇 — Rc<i32> 는 스레드 사이에 안전하게 보낼 수 없다(cannot be sent between threads safely).
  2. 어디 — 7번째 줄의 thread::spawn 에 넘긴 클로저. 그 안에 Rc 가 들어 있다.
  3. 고치는 법 — help 가 이유를 말한다. Rc<i32> 는 Send 트레이트를 갖추지 않았다.

Send 는 "다른 스레드로 보내도 안전하다" 는 트레이트(33편)다. Rc 는 주인 수를 셀 때 동시에 셀 것을 대비하지 않아서 빠르지만, 두 스레드가 동시에 세면 수가 틀어질 수 있다. 그래서 Send 가 아니고, 컴파일러가 막는다. 스레드용은 Arc(Atomic Rc)다. 쓰는 법은 Rc 와 같고, 수를 안전하게 센다.

02 Mutex — 한 번에 한 스레드만

Arc 도 Rc 처럼 안을 고칠 수 없다. 고치려면 Mutex 로 감싼다. 먼저 스레드 없이 써 보자.

rust
use std::sync::Mutex;

fn main() {
    let count = Mutex::new(0);
    {
        let mut n = count.lock().unwrap();
        *n += 1;
    }
    println!("{:?}", count.lock().unwrap());
}
text
   Compiling counter v0.1.0 (~/projects/counter)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/counter`
1
  • Mutex::new(0) 은 0 을 자물쇠 안에 넣는다.
  • count.lock() 은 자물쇠를 연다. 다른 스레드가 이미 열고 있으면 닫힐 때까지 기다린다. Result 를 주므로 unwrap 했다.
  • 연 결과 n 으로 안의 값을 고친다(*n += 1). 39편의 borrow_mut 와 비슷하다.
  • n 이 블록을 나가면 자물쇠가 저절로 닫힌다. 닫는 코드를 따로 쓰지 않아서 잊을 일이 없다.

03 실습 — 스레드 넷이 함께 세기

rust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let count = Arc::new(Mutex::new(0));
    let mut handles = Vec::new();

    for _ in 0..4 {
        let count = Arc::clone(&count);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let mut n = count.lock().unwrap();
                *n += 1;
            }
        }));
    }

    for h in handles {
        h.join().unwrap();
    }
    println!("count: {}", *count.lock().unwrap());
}
text
   Compiling counter v0.1.0 (~/projects/counter)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/counter`
count: 4000
  • Arc::new(Mutex::new(0)) 은 "여러 스레드가 함께 갖는, 자물쇠 달린 정수" 다.
  • 스레드마다 Arc::clone(&count) 를 하나씩 move 로 가져간다. 39편의 Rc::clone 과 같다.
  • 스레드 넷이 각각 1000번 lock 해서 1 을 더했다. 몇 번을 돌려도 4000 이다.

자물쇠 없이 여러 스레드가 한 값을 동시에 고치면, 같은 값을 읽고 각자 1 을 더해 쓰면서 더한 것이 사라지는 일(데이터 경쟁)이 생긴다. 다른 언어에서는 가끔씩만 틀려서 찾기 어려운 버그다. Rust 에서는 Mutex 없이 고치는 코드를 아예 컴파일할 수 없다. 1편에서 말한 "동시성 버그까지 컴파일할 때 막는다" 가 이것이다.

무엇을한 스레드여러 스레드
주인 여럿Rc (39편)Arc
안을 고치기RefCell (39편)Mutex

다음 편은 스레드 대신 기다리는 일을 겹쳐 처리하는 async 다.