21. 스레드·채널·Arc/Mutex
Rust 가 fearless concurrency 를 표방하는 이유는 빌림 검사기가 멀티스레드 데이터 경합까지 컴파일 타임에 막기 때문입니다. 이 강의에서 std::thread, channel(mpsc) 로 스레드 간 메시지, 그리고 공유 상태가 필요할 때의 Arc<Mutex<T>> 패턴까지 한 번에 정리합니다.
이 강의에서 배우는 것
- 1std::thread::spawn 으로 스레드를 시작하고 .join 으로 기다린다
- 2move 클로저로 변수를 스레드에 넘긴다
- 3mpsc 채널로 스레드 간 메시지를 주고받는다
- 4Arc<Mutex<T>> 패턴으로 공유 상태를 안전하게 변경한다
- 5Send / Sync 트레잇의 역할을 안다
소개
Rust 의 동시성은 "실수해도 안전" 이 핵심 약속입니다. 빌림 규칙이 단일 스레드에서 막던 것(가변 참조 1개) 이 멀티스레드까지 그대로 확장되어, 데이터 경합이 **컴파일 타임에** 차단됩니다. 그 결과 락을 빠뜨려서 생기는 데이터 경합 디버깅 시간이 극적으로 줄어듭니다.
핵심 개념
1) 스레드 생성
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` 키워드로 소유권 자체를 이동시켜 해결.
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 을 워커 스레드에서 보내고 메인에서 받기:
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
}여러 스레드가 카운터를 안전하게 증가:
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~25, 26~50, 51~75, 76~100 합을 mpsc 로 메인에 전달
- Arc<Mutex<Vec<i32>>> 를 만들어 여러 스레드에서 push 가 안전한지 확인
- Rc 를 thread::spawn 에 넘기는 코드를 일부러 작성해 컴파일 에러 메시지 확인