← Rust 강의 목록으로
🦀
모던 Rust
모던 · 선수: 20강

21. 스레드·채널·Arc/Mutex

Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.

RustthreadconcurrencyArcMutexchannelmpsc
소요 시간
약 2시간
난이도
📊 중급
선수 조건
🎯 20강
결과물
Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.

이 강의에서 배우는 것

  • 1std::thread::spawn 으로 스레드를 시작하고 .join 으로 기다린다
  • 2move 클로저로 변수를 스레드에 넘긴다
  • 3mpsc 채널로 스레드 간 메시지를 주고받는다
  • 4Arc<Mutex<T>> 패턴으로 공유 상태를 안전하게 변경한다
  • 5Send / Sync 트레잇의 역할을 안다

소개

Rust 의 동시성은 "실수해도 안전" 이 핵심 약속입니다. 빌림 규칙이 단일 스레드에서 막던 것(가변 참조 1개) 이 멀티스레드까지 그대로 확장되어, 데이터 경합이 **컴파일 타임에** 차단됩니다. 그 결과 락을 빠뜨려서 생기는 데이터 경합 디버깅 시간이 극적으로 줄어듭니다.

핵심 개념

1) 스레드 생성

rust
use std::thread;
use std::time::Duration;

fn main() {
    let h = thread::spawn(|| {
        for i in 1..=3 {
            println!("child {}", i);
            thread::sleep(Duration::from_millis(50));
        }
    });
    for i in 1..=3 {
        println!("main {}", i);
        thread::sleep(Duration::from_millis(50));
    }
    h.join().unwrap();
}

2) move 클로저로 변수 이동

클로저가 외부 변수를 빌리면 스레드 수명을 컴파일러가 보장 못 합니다. `move` 키워드로 소유권 자체를 이동시켜 해결.

rust
let v = vec![1, 2, 3];
let h = thread::spawn(move || println!("{:?}", v));
// 여기서 v 는 이미 move 됨
h.join().unwrap();

3) mpsc 채널 — 메시지 패싱

multi-producer single-consumer 표준 채널. 송신자(Sender) 가 여러 개, 수신자(Receiver) 가 하나.

4) Arc<Mutex<T>> — 공유 가변 상태

  • **Arc<T>** — 여러 스레드에서 공유 가능한 reference count (atomic)
  • **Mutex<T>** — 한 시점에 하나만 가변 접근 (런타임 락)
  • **조합 Arc<Mutex<T>>** — 여러 스레드가 공유하면서 가변 접근

5) Send / Sync 트레잇

트레잇의미
Send스레드 간 소유권 이전 안전
Sync여러 스레드에서 동시 참조 안전

거의 모든 표준 타입에 자동 derive 됩니다. Rc 와 RefCell 은 Send/Sync 가 아니라 스레드 간 전달 시 컴파일 에러 — 그 자리에 Arc/Mutex 를 쓰면 됩니다.

핵심 예제

채널로 1~10 을 워커 스레드에서 보내고 메인에서 받기:

rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel::<i32>();
    thread::spawn(move || {
        for i in 1..=10 { tx.send(i).unwrap(); }
    });
    let sum: i32 = rx.iter().sum();
    println!("sum = {}", sum); // 55
}

여러 스레드가 카운터를 안전하게 증가:

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut hs = vec![];
    for _ in 0..10 {
        let c = Arc::clone(&counter);
        hs.push(thread::spawn(move || {
            let mut n = c.lock().unwrap();
            *n += 1;
        }));
    }
    for h in hs { h.join().unwrap(); }
    println!("counter = {}", *counter.lock().unwrap()); // 10
}

자주 하는 실수

Q. Rc 를 스레드 사이에 넘기면 안 되나요?

A. 안 됩니다 — Rc 는 non-atomic ref count 라 멀티스레드에서 경합 발생. Arc 로 바꾸면 됩니다 (atomic operation). 컴파일러가 Send 가 아니라고 막아 줍니다.

Q. Mutex 가 락 풀리는 시점은?

A. `lock()` 으로 얻은 가드(MutexGuard) 가 **drop 되는 시점**. 보통 스코프 끝. 명시적으로 빨리 풀려면 `drop(guard)` 또는 별도 블록으로 묶어 스코프를 좁힙니다.

Q. async/await 가 대신 쓸 수 있나요?

A. CPU 작업은 스레드, IO 중심은 async 가 일반적. 다음 강의에서 async/await + tokio 로 IO 비동기를 다룹니다.

정리

  • thread::spawn + move 클로저로 스레드 생성
  • mpsc 채널로 메시지 패싱
  • 공유 가변 상태 = Arc<Mutex<T>>
  • Send/Sync 트레잇으로 컴파일 타임 데이터 경합 검사

과제

  1. 네 개 워커 스레드를 만들고 각각 1~25, 26~50, 51~75, 76~100 합을 mpsc 로 메인에 전달
  2. Arc<Mutex<Vec<i32>>> 를 만들어 여러 스레드에서 push 가 안전한지 확인
  3. Rc 를 thread::spawn 에 넘기는 코드를 일부러 작성해 컴파일 에러 메시지 확인
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗