Rust 기초부터 마스터까지 — 9화
스레드, 채널, Mutex로 동시성 구성하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
Rust는 데이터 경쟁을 만들 수 있는 공유 접근을 타입으로 제한한다. 스레드, 채널, Mutex를 사용하더라도 작업 종료와 잠금 범위는 직접 설계해야 한다.
스레드 시작과 종료
use std::thread;
let handle = thread::spawn(|| {
load_world_data()
});
let world = handle
.join()
.expect("worker thread panicked");
spawn에 전달한 클로저는 새 스레드에서 실행된다. join은 작업 완료를 기다리고 panic 여부를 결과로 돌려준다.
클로저가 바깥 값을 사용한다면 스레드보다 오래 살아야 하므로 move로 소유권을 넘기는 경우가 많다.
채널로 메시지 보내기
use std::sync::mpsc;
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
sender.send(String::from("Loaded")).unwrap();
});
if let Ok(message) = receiver.recv() {
println!("{message}");
}
채널은 값을 공유하는 대신 한 작업에서 다른 작업으로 메시지를 이동시킨다. 송신자가 모두 사라지면 수신자는 채널이 닫혔음을 알 수 있다.
Arc와 Mutex
use std::sync::{Arc, Mutex};
let score = Arc::new(Mutex::new(0));
let worker_score = Arc::clone(&score);
let handle = thread::spawn(move || {
let mut value = worker_score.lock().unwrap();
*value += 10;
});
handle.join().unwrap();
Arc는 소유권을 스레드 사이에서 공유하고 Mutex는 한 번에 하나의 변경 접근만 허용한다. 잠금을 오래 잡거나 여러 잠금을 다른 순서로 얻으면 성능 저하와 교착 상태가 생길 수 있다.
동시성은 스레드 수를 늘리는 작업이 아니다. 공유 상태를 줄이고 메시지 경계를 정한 뒤, 작업 크기와 대기 시간을 실제로 측정해야 한다.