스레드와 공유 상태 - Arc·Mutex·채널
이 장에서 배우는 것
앞 장에서는 구조체 속 참조의 수명과 'static 에 대한 오해를 정리했다. 이 장에서는 그 지식을 스레드(thread)로 옮긴다. 스레드는 호출한 함수보다 오래 살 수 있어서, 빌림 규칙이 가장 엄격하게 적용되는 곳이다. 컴파일러가 막아 주는 지점을 하나씩 짚으며 창고 재고 처리를 여러 스레드로 나눈다. 실행할 때마다 순서가 달라지는 출력을 어떻게 결정적으로 만드는지도 다룬다.
thread::spawn과join으로 스레드를 만들고 결과와 패닉을 받는다.- 스코프 스레드(scoped thread)로
Arc없이 지역 데이터를 빌려 쓴다. Arc<Mutex<T>>로 재고 같은 공유 상태를 안전하게 갱신한다.mpsc채널(channel)로 결과를 모으고, 정렬로 출력 순서를 고정한다.Send,Sync의 의미를 읽고 교착 상태(deadlock)를 피하는 잠금 순서를 정한다.
문제 상황
창고에 주문이 일곱 건 들어왔다. 품목은 볼트, 너트, 와셔 세 가지이고 재고는 각각 50, 30, 10개다. 주문을 한 줄로 처리하면 느리지 않은 규모이지만, 실제로는 주문마다 검수와 로그 기록이 붙어 시간이 걸린다고 가정하자. 그래서 품목마다 작업자 스레드를 하나씩 두고 같은 재고 장부를 갱신하게 하려 한다.
여기서 두 가지 문제가 생긴다. 첫째, 여러 스레드가 한 장부를 동시에 고치면 데이터 경쟁(data race)이 생길 수 있다. 둘째, 스레드가 끝나는 순서는 실행할 때마다 달라서 "주문 3 처리 완료" 같은 로그가 뒤섞인다. 로그가 뒤섞이면 테스트도 디버깅도 어렵다. 이 장의 목표는 병렬로 처리하되 같은 입력에 항상 같은 출력이 나오게 하는 것이다.
스레드 만들기: spawn, join, scope
spawn 은 'static 을 요구한다
thread::spawn 은 클로저를 받아 새 스레드에서 실행하고 JoinHandle 을 돌려준다. 새 스레드는 만든 함수가 끝난 뒤에도 살아 있을 수 있다. 그래서 클로저는 지역 변수를 빌릴 수 없고, 값의 소유권을 move 로 가져가야 한다.
use std::thread;
fn main() {
let qtys: Vec<u32> = vec![50, 30, 10];
let h = thread::spawn(|| qtys.iter().sum::<u32>());
println!("{}", h.join().unwrap());
}
컴파일러는 E0373 으로 거절한다. 요지는 "클로저가 현재 함수보다 오래 살 수 있는데 함수가 소유한 qtys 를 빌린다"는 것이다. 클로저 앞에 move 를 붙이면 qtys 가 스레드로 넘어가고 오류가 사라진다.
join 은 스레드가 끝날 때까지 기다리고 Result 를 돌려준다. 스레드가 패닉했다면 Err 이고, 정상 종료했다면 클로저의 반환값이 Ok 에 담긴다. 예제는 unwrap 으로 패닉을 그대로 전파하지만, 실무에서는 Err 를 받아 해당 작업만 실패로 기록하기도 한다.
스코프 스레드는 지역 데이터를 빌린다
thread::scope 안에서 만든 스레드는 스코프가 끝나기 전에 모두 합류하는 것이 보장된다. 그래서 컴파일러가 'static 을 요구하지 않고, 지역 변수를 참조로 넘길 수 있다. 읽기만 하는 병렬 계산에는 Arc 로 감쌀 필요가 없다. 완성 코드의 품목별 주문 수량 합산이 이 방식이다.
| 구분 | thread::spawn | thread::scope | 선택 기준 |
|---|---|---|---|
| 클로저 조건 | 'static, 보통 move | 스코프보다 오래 사는 참조면 충분 | 지역 데이터를 쓰면 scope |
| 합류 시점 | 직접 join | 스코프 종료 시 자동 join | 누락을 막으려면 scope |
| 스레드 수명 | 함수를 넘어 살 수 있음 | 스코프 안에서만 | 백그라운드 작업이면 spawn |
공유 상태: Arc, Mutex, 채널
Arc<Mutex<T>>
스레드 여러 개가 같은 장부를 가지려면 소유자가 여럿이어야 한다. Arc 는 참조 횟수를 원자적으로 세는 공유 소유 포인터다. 앞서 본 단일 스레드용 공유 포인터와 달리 횟수 증감이 스레드에 안전하다. 다만 Arc 는 내용을 불변으로만 보여 준다. 고치려면 Mutex(뮤텍스, 상호 배제 잠금)가 필요하다.
lock() 은 잠금을 얻을 때까지 기다렸다가 MutexGuard 를 돌려준다. 가드가 스코프를 벗어나 drop 되면 잠금이 풀린다. 잠금을 푸는 코드를 따로 쓰지 않으므로 풀기를 잊는 실수가 없다. 반대로 말하면, 가드가 살아 있는 동안은 계속 잠겨 있다. lock() 이 Result 를 돌려주는 이유는 중독(poisoning) 때문이다. 잠금을 쥔 스레드가 패닉하면 그 뮤텍스는 중독 상태가 되고 이후 lock() 이 Err 를 돌려준다. 이 장은 unwrap 으로 처리한다.
잠금 범위는 짧을수록 좋다. 이 장의 process 함수는 재고 확인과 차감을 가드 하나로 끝낸다. 확인과 차감 사이에 잠금을 풀면 그 틈에 다른 스레드가 같은 재고를 가져가는 경쟁이 생긴다. 확인과 갱신은 한 가드 안에서 해야 한다.
채널로 결과 모으기
mpsc::channel 은 다중 생산자 단일 소비자(multi-producer, single-consumer) 채널이다. 송신 쪽 Sender 는 clone 으로 늘리고, 수신 쪽 Receiver 는 하나뿐이다. 작업자는 결과를 보내기만 하고 장부 바깥의 출력 순서에는 관여하지 않는다. 수신 쪽이 모아서 한 번에 정렬해 출력한다.
수신 반복은 모든 Sender 가 drop 되어야 끝난다. 원래 만든 tx 를 남겨 두면 작업자가 모두 끝나도 반복이 끝나지 않는다. 복제본을 나눠 준 뒤 원본은 drop(tx) 로 버려야 한다.
| 방식 | 데이터의 위치 | 동기화 지점 | 어울리는 경우 |
|---|---|---|---|
| Arc<Mutex<T>> | 모두가 한 곳을 본다 | lock 호출 | 재고 장부처럼 모두가 갱신 |
| mpsc 채널 | 값이 스레드 사이를 이동한다 | send, recv | 작업 결과 수집, 로그 |
| 스코프 + 참조 | 호출자가 소유한다 | 스코프 종료 | 읽기 전용 병렬 계산 |
Send 와 Sync, 그리고 교착 상태
Send 와 Sync 가 뜻하는 것
Send 는 값의 소유권을 다른 스레드로 옮겨도 안전하다는 표지이고, Sync 는 &T 를 여러 스레드가 동시에 가져도 안전하다는 표지다. 정확히는 T: Sync 와 &T: Send 가 같은 말이다. 둘 다 컴파일러가 필드를 보고 자동으로 붙이는 마커 트레이트이고, 직접 구현할 일은 unsafe 를 다루는 뒤 장에서나 생긴다. thread::spawn 은 클로저가 Send 일 것을 요구하므로, 안전하지 않은 공유는 컴파일 단계에서 걸린다.
use std::rc::Rc;
use std::thread;
fn main() {
let book = Rc::new(5);
thread::spawn(move || println!("{book}"));
}
오류의 요지는 "Rc<i32> 는 스레드 사이에서 안전하게 보낼 수 없다. Send 트레이트가 구현되지 않았다"이다. Rc 의 참조 횟수가 원자적이지 않아서 두 스레드가 동시에 증감하면 횟수가 틀어지기 때문이다. Arc 로 바꾸면 해결된다.
| 타입 | Send | Sync | 이유 |
|---|---|---|---|
| Rc<T> | 아니오 | 아니오 | 참조 횟수가 원자적이지 않다 |
| Arc<T> | T 가 Send 이면서 Sync | 같음 | 횟수는 안전하지만 T 는 여러 곳에서 보인다 |
| RefCell<T> | T 가 Send 이면 예 | 아니오 | 빌림 검사가 스레드에 안전하지 않다 |
| Mutex<T> | T 가 Send 이면 예 | T 가 Send 이면 예 | 잠금이 접근을 직렬화한다 |
같은 이유로 Arc<RefCell<i32>> 도 스레드로 보낼 수 없다. 오류의 요지는 "RefCell<i32> 는 스레드 사이에서 안전하게 공유할 수 없다. Sync 가 구현되지 않았다"이다. 내부 가변성이 필요하면 RefCell 대신 Mutex 를 쓴다.
교착 상태 피하기
잠금이 둘 이상이면 교착 상태가 가능하다. 스레드 1은 선반 A 를 쥐고 B 를 기다리고, 스레드 2는 B 를 쥐고 A 를 기다리면 둘 다 영영 진행하지 못한다. 컴파일러는 이를 잡지 못한다. 규칙으로 막아야 한다.
가장 단순한 규칙은 모든 스레드가 같은 순서로 잠그는 것이다. 완성 코드의 move_units 는 두 선반을 이름 순으로 잠근다. 옮기는 방향이 A 에서 B 든 B 에서 A 든 잠그는 순서는 항상 A, B 다. 같은 선반을 출발지와 도착지로 동시에 받으면 한 스레드가 자기 자신을 기다리므로 먼저 거른다. 그 밖의 원칙도 있다. 잠금을 쥔 채 join 이나 recv 로 다른 스레드를 기다리지 않는다. 잠금을 쥔 채 오래 걸리는 일을 하지 않는다.
출력 순서를 결정적으로 만드는 방법
스레드가 도는 순서는 운영체제가 정한다. 그래서 스레드 안에서 곧바로 println! 하면 순서가 보장되지 않는다. 이 장은 세 가지를 쓴다. 첫째, 스레드 안에서는 출력하지 않고 값을 돌려주거나 채널로 보낸다. 둘째, 모은 결과를 주문 번호 같은 안정적인 키로 정렬한 뒤 한 곳에서 출력한다. 셋째, 결과가 처리 순서에 좌우되는 작업은 키별로 한 스레드에 몰아 순서를 고정한다. 같은 품목의 주문을 한 작업자가 번호순으로 처리하면, 품목 사이의 실행 순서가 어떻든 재고 결과는 같다.
완성 코드
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;
const INITIAL: [(&str, u32); 3] = [("bolt", 50), ("nut", 30), ("washer", 10)];
struct Order {
id: u32,
sku: &'static str,
qty: u32,
}
enum Outcome {
Shipped { id: u32, sku: &'static str, qty: u32 },
Rejected { id: u32, sku: &'static str, missing: u32 },
}
impl Outcome {
fn id(&self) -> u32 {
match self {
Outcome::Shipped { id, .. } | Outcome::Rejected { id, .. } => *id,
}
}
}
type Stock = Arc<Mutex<HashMap<&'static str, u32>>>;
fn process(stock: &Stock, order: &Order) -> Outcome {
let mut map = stock.lock().unwrap();
let have = map.entry(order.sku).or_insert(0);
if *have >= order.qty {
*have -= order.qty;
Outcome::Shipped { id: order.id, sku: order.sku, qty: order.qty }
} else {
Outcome::Rejected { id: order.id, sku: order.sku, missing: order.qty - *have }
}
}
struct Shelf {
name: &'static str,
qty: Mutex<u32>,
}
fn move_units(from: &Shelf, to: &Shelf, n: u32) -> bool {
if from.name == to.name {
return false;
}
let from_first = from.name < to.name;
let (first, second) = if from_first { (from, to) } else { (to, from) };
let mut g1 = first.qty.lock().unwrap();
let mut g2 = second.qty.lock().unwrap();
let (src, dst) = if from_first {
(&mut *g1, &mut *g2)
} else {
(&mut *g2, &mut *g1)
};
if *src < n {
return false;
}
*src -= n;
*dst += n;
true
}
fn shuttle(from: &Shelf, to: &Shelf, times: u32) -> u32 {
let mut done = 0;
for _ in 0..times {
if move_units(from, to, 1) {
done += 1;
}
}
done
}
fn main() {
// 1. spawn 과 join
let qtys: Vec<u32> = INITIAL.iter().map(|&(_, q)| q).collect();
let summer = thread::spawn(move || qtys.iter().sum::<u32>());
let total = summer.join().unwrap();
println!("초기 재고 합계: {total}");
let orders = vec![
Order { id: 1, sku: "bolt", qty: 20 },
Order { id: 2, sku: "nut", qty: 25 },
Order { id: 3, sku: "bolt", qty: 20 },
Order { id: 4, sku: "washer", qty: 15 },
Order { id: 5, sku: "nut", qty: 10 },
Order { id: 6, sku: "bolt", qty: 20 },
Order { id: 7, sku: "washer", qty: 5 },
];
let skus = ["bolt", "nut", "washer"];
// 2. 스코프 스레드로 품목별 주문 수량 합산
let demand: Vec<(&str, u32)> = thread::scope(|s| {
let orders_ref = &orders;
let handles: Vec<_> = skus
.iter()
.map(move |&sku| {
s.spawn(move || {
let sum = orders_ref
.iter()
.filter(|o| o.sku == sku)
.map(|o| o.qty)
.sum::<u32>();
(sku, sum)
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
let line: Vec<String> = demand.iter().map(|(s, n)| format!("{s}={n}")).collect();
println!("품목별 주문 수량: {}", line.join(" "));
// 3. Arc<Mutex> 장부 + 채널로 결과 수집
let stock: Stock = Arc::new(Mutex::new(INITIAL.iter().copied().collect()));
let orders = Arc::new(orders);
let (tx, rx) = mpsc::channel::<Outcome>();
let mut workers = Vec::new();
for sku in skus {
let tx = tx.clone();
let stock = Arc::clone(&stock);
let orders = Arc::clone(&orders);
workers.push(thread::spawn(move || {
for order in orders.iter().filter(|o| o.sku == sku) {
tx.send(process(&stock, order)).unwrap();
}
}));
}
drop(tx);
let mut outcomes: Vec<Outcome> = rx.iter().collect();
for w in workers {
w.join().unwrap();
}
outcomes.sort_by_key(|o| o.id());
for o in &outcomes {
match o {
Outcome::Shipped { id, sku, qty } => println!("주문 {id}: {sku} {qty}개 출고"),
Outcome::Rejected { id, sku, missing } => {
println!("주문 {id}: {sku} {missing}개 부족, 거절")
}
}
}
println!("Arc 참조 수: {}", Arc::strong_count(&stock));
{
let guard = stock.lock().unwrap();
let mut rows: Vec<_> = guard.iter().collect();
rows.sort();
for (sku, qty) in rows {
println!("{sku}: {qty}");
}
}
// 4. 잠금 순서를 맞춘 선반 이동
let a = Shelf { name: "A", qty: Mutex::new(100) };
let b = Shelf { name: "B", qty: Mutex::new(100) };
let (ab, ba) = thread::scope(|s| {
let t1 = s.spawn(|| shuttle(&a, &b, 100));
let t2 = s.spawn(|| shuttle(&b, &a, 100));
(t1.join().unwrap(), t2.join().unwrap())
});
println!("이동 성공: A→B {ab}회, B→A {ba}회");
println!("선반 A={} B={}", a.qty.lock().unwrap(), b.qty.lock().unwrap());
}
줄별 해설
상수와 타입. INITIAL 은 초기 재고 상수다. Order 의 sku 가 &'static str 인 이유는 문자열 리터럴만 담기 때문이며, 이 덕분에 작업자 스레드로 넘겨도 수명 문제가 없다. Outcome 은 출고와 거절을 구분하는 열거형이고, id 메서드는 정렬 키를 꺼낸다. 두 변형의 id 를 한 패턴으로 묶는 or 패턴이 쓰였다.
process. lock().unwrap() 으로 가드를 얻고, entry(...).or_insert(0) 로 품목의 수량 참조를 얻는다. 재고가 충분하면 차감하고 Shipped, 아니면 부족분을 계산해 Rejected 를 돌려준다. 확인과 차감이 같은 가드 안에 있어서 중간에 다른 스레드가 끼어들지 못한다. 가드는 함수가 끝날 때 drop 되며 잠금이 풀린다.
move_units. 이름을 비교해 항상 사전순으로 앞선 선반을 먼저 잠근다. from_first 로 가드가 출발지인지 도착지인지 다시 맞춰 src, dst 가변 참조를 만든다. 재고가 모자라면 일찍 반환하고, 그 경우에도 가드는 자동으로 풀린다. shuttle 은 이동을 반복하고 성공 횟수를 센다.
1단계: spawn 과 join. qtys 를 move 클로저로 넘기고, join().unwrap() 으로 합계를 받는다. 값은 스레드의 반환값으로 돌아오므로 공유 상태가 필요 없다.
2단계: 스코프 스레드. orders_ref 는 orders 에 대한 참조다. 참조는 복사되므로 move 클로저 안팎에서 여러 번 쓸 수 있다. 품목마다 스레드 하나가 filter 로 자기 품목의 수량을 합한다. 스코프가 orders 보다 먼저 끝나므로 Arc 가 필요 없다. 핸들을 모아 skus 순서대로 join 하기 때문에 결과 순서는 항상 볼트, 너트, 와셔다.
3단계: 장부와 채널. 스코프가 끝나 orders 의 빌림이 모두 풀렸으므로 Arc::new(orders) 로 소유권을 옮긴다. 반복마다 tx, stock, orders 를 복제해 이름을 가린(shadowing) 뒤 move 로 스레드에 준다. 한 작업자는 한 품목의 주문만 번호순으로 처리하고 결과를 보낸다. 반복이 끝나면 drop(tx) 로 원본 송신자를 버려 수신 반복이 끝나게 한다. rx.iter().collect() 는 모든 결과가 올 때까지 기다린다. 이어서 작업자를 join 하고, sort_by_key 로 주문 번호순 정렬을 한 뒤 출력한다. 작업자들이 모두 합류했으므로 Arc::strong_count 는 1 이다. 마지막 블록에서는 장부를 잠그고 항목을 정렬해 출력한다. HashMap 순회 순서는 정해져 있지 않으므로 정렬이 필수다.
4단계: 선반 이동. 두 스레드가 서로 반대 방향으로 100번씩 옮긴다. 각 방향의 출발 선반은 100개로 시작하므로 100번 모두 성공하고 최종 수량은 처음과 같다. 잠금 순서가 같으므로 어떤 교차 실행에서도 교착이 생기지 않는다. 마지막 출력은 두 가드를 한 문장 안에서 얻지만 서로 다른 뮤텍스라 문제가 없다.
실행 결과
$ cargo run
초기 재고 합계: 90
품목별 주문 수량: bolt=60 nut=35 washer=20
주문 1: bolt 20개 출고
주문 2: nut 25개 출고
주문 3: bolt 20개 출고
주문 4: washer 5개 부족, 거절
주문 5: nut 5개 부족, 거절
주문 6: bolt 10개 부족, 거절
주문 7: washer 5개 출고
Arc 참조 수: 1
bolt: 10
nut: 5
washer: 5
이동 성공: A→B 100회, B→A 100회
선반 A=100 B=100
몇 번을 실행해도 같은 출력이 나온다. 스레드의 실행 순서는 바뀌지만, 결과를 정렬해서 출력하고 같은 품목은 한 스레드가 순서대로 처리하기 때문이다.
실무에서 자주 틀리는 것
1. move 없이 지역 변수를 spawn 에 넘긴다
틀린 코드다. 오류 요지는 "클로저가 현재 함수보다 오래 살 수 있지만 names 를 빌린다"이다.
let names = vec![String::from("bolt")];
let h = thread::spawn(|| println!("{}", names.len()));
h.join().unwrap();
고친 코드는 두 가지다. 소유권을 넘기거나, 이후에도 names 를 써야 하면 스코프 스레드로 빌린다.
let names = vec![String::from("bolt")];
thread::scope(|s| {
s.spawn(|| println!("{}", names.len()));
});
println!("{}", names.len());
2. 스레드 안에서 바로 출력해 순서가 흔들린다
틀린 코드는 실행마다 줄 순서가 달라질 수 있다.
for order in orders.iter() {
let stock = Arc::clone(&stock);
thread::spawn(move || println!("{}", process_and_format(&stock)));
}
고친 방식은 스레드가 문자열이나 결과 값을 채널로 보내고, 메인 스레드가 키로 정렬해 한 번에 출력하는 것이다. 위 완성 코드의 outcomes.sort_by_key 가 그 예다. 합류하지 않은 스레드는 메인이 끝나면 함께 종료되므로, 핸들을 모아 join 하는 것도 잊지 않아야 한다.
3. 가드를 쥔 채 다른 스레드를 기다린다
틀린 코드다. 메인이 잠금을 쥔 채 join 하고, 스레드는 같은 잠금을 기다리므로 영원히 끝나지 않는다.
let guard = stock.lock().unwrap();
let s2 = Arc::clone(&stock);
let h = thread::spawn(move || s2.lock().unwrap().len());
h.join().unwrap();
println!("{}", guard.len());
고친 코드는 잠금 범위를 블록으로 줄이거나, 잠금이 필요 없어진 뒤에 join 한다.
let h = {
let s2 = Arc::clone(&stock);
thread::spawn(move || s2.lock().unwrap().len())
};
let n = h.join().unwrap();
println!("{}", n);
4. 원본 Sender 를 drop 하지 않아 수신이 끝나지 않는다
틀린 코드는 컴파일은 되지만 멈춘 채로 있다.
let (tx, rx) = mpsc::channel::<u32>();
for i in 0..3 {
let tx = tx.clone();
thread::spawn(move || tx.send(i).unwrap());
}
let all: Vec<u32> = rx.iter().collect(); // tx 원본이 살아 있어 끝나지 않는다
고친 코드는 복제본을 나눠 준 뒤 원본을 버리고, 결과를 정렬해 순서를 고정한다.
let (tx, rx) = mpsc::channel::<u32>();
for i in 0..3 {
let tx = tx.clone();
thread::spawn(move || tx.send(i).unwrap());
}
drop(tx);
let mut all: Vec<u32> = rx.iter().collect();
all.sort();
한눈에 보기
| 도구 | 역할 | 주의할 점 | 이 장의 예 |
|---|---|---|---|
| thread::spawn + join | 독립 스레드와 결과 회수 | move 필요, join 은 Result | 초기 재고 합계 |
| thread::scope | 지역 데이터를 빌려 병렬 실행 | 스코프 안에서만 유효 | 품목별 수량 합산 |
| Arc<Mutex<T>> | 공유 가변 상태 | 잠금 범위는 짧게 | 재고 장부 |
| mpsc 채널 | 결과 전달 | 원본 Sender drop | 출고 결과 수집 |
| Send, Sync | 스레드 안전성 표지 | Rc, RefCell 은 제외 | 컴파일 오류로 확인 |
| 잠금 순서 고정 | 교착 방지 | 자기 자신 잠금 주의 | 선반 이동 |
| 방법 | 어떻게 | 비용 | 한계 |
|---|---|---|---|
| 스레드 안에서 출력 금지 | 값을 반환하거나 전송 | 없음 | 호출자가 출력해야 한다 |
| 정렬 후 출력 | 안정적인 키로 sort | 정렬 시간 | 모든 결과를 모아야 한다 |
| 키별 한 스레드 | 같은 품목은 순서대로 | 병렬도 감소 | 키 사이 의존이 없을 때만 |
연습 문제
- 다음 코드는 컴파일되지 않는다. 원인을 말하고,
move를 쓰는 방법과 스코프 스레드를 쓰는 방법으로 각각 고쳐라.let skus = vec!["bolt", "nut"]; let h = thread::spawn(|| skus.len()); println!("{:?}", skus); Arc<RefCell<u32>>를 스레드 네 개가 1씩 더하는 용도로 쓰려 한다. 컴파일 오류의 원인을 설명하고, 고친 타입을 쓰라.- 스레드 1은
a.lock()다음b.lock(), 스레드 2는b.lock()다음a.lock()을 호출한다. 교착이 생기는 조건을 설명하고 한 줄 규칙으로 해결하라. - 스레드 세 개가 각각
(i, i * i)를 채널로 보낸다(i는 1, 2, 3). 매번 같은 순서로 한 줄씩 출력하는 코드를 써라.
정답과 해설
- 원인은
spawn의 클로저가skus를 빌리는데, 스레드가 함수보다 오래 살 수 있기 때문이다. 게다가 뒤에서skus를 다시 쓴다.move로 고치려면 소유권을 넘기므로 뒤의 출력은 쓸 수 없다. 이후에 필요하면let copy = skus.clone();을 만들어move로 넘긴다. 스코프 스레드로 고치면thread::scope(|s| { s.spawn(|| skus.len()); });이후에도skus를 그대로 쓸 수 있다. RefCell은Sync가 아니어서Arc<RefCell<u32>>는Send가 되지 못하고spawn이 거절한다. 빌림 검사가 스레드에 안전하지 않기 때문이다.Arc<Mutex<u32>>로 바꾸고*counter.lock().unwrap() += 1;로 더한다.- 스레드 1이 a 를, 스레드 2가 b 를 쥔 상태에서 서로 상대의 잠금을 기다리면 대기가 순환해 둘 다 진행하지 못한다. 규칙은 "모든 스레드가 항상 a 다음 b 순서로 잠근다"이다. 순서가 같으면 먼저 a 를 얻은 쪽만 b 로 넘어가므로 순환이 생기지 않는다.
-
도착 순서는 달라질 수 있지만use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel::<(u32, u32)>(); let mut hs = Vec::new(); for i in 1..=3 { let tx = tx.clone(); hs.push(thread::spawn(move || tx.send((i, i * i)).unwrap())); } drop(tx); let mut rows: Vec<(u32, u32)> = rx.iter().collect(); for h in hs { h.join().unwrap(); } rows.sort(); for (i, sq) in rows { println!("{i}: {sq}"); } }sort후 출력하므로 항상1: 1,2: 4,3: 9가 나온다.drop(tx)가 없으면collect가 끝나지 않는다.
동기화 도구의 표준 설명은 std::sync 문서에서 확인할 수 있다. 다음에는 연관 타입과 newtype 으로 제네릭을 더 정밀하게 다루는 방법을 살펴본다.