Rust 동시성
동시성(concurrency)은 여러 작업을 한꺼번에 실행하는 거예요. 많은 언어에서 동시성은 정말 위험한 영역이에요. 두 스레드가 같은 데이터를 동시에 건드리면 데이터가 망가질 수 있는데, 이런 버그는 원인을 찾기가 너무 힘들거든요. 대여 규칙이 막으려고 만든 게 바로 이런 버그이고, 그 보호는 스레드에도 그대로 적용돼요.
스레드 만들기
섹션 제목: “스레드 만들기”use std::thread;
fn main() { let handle = thread::spawn(|| { for i in 1..=3 { println!("스레드: {i}"); } });
for i in 1..=3 { println!("메인: {i}"); }
handle.join().unwrap(); // 스레드가 끝날 때까지 기다려요}thread::spawn은 클로저를 받아서 새 스레드에서 실행해요. 다른 코드와 나란히, 동시에 돌아가요. .join()은 그 스레드가 끝날 때까지 기다려요. 이게 없으면 새로 만든 스레드가 실행되기도 전에 main이 끝나 버릴 수 있어요.
스레드로 데이터 옮기기
섹션 제목: “스레드로 데이터 옮기기”thread::spawn에 넘기는 클로저는 자신이 쓰는 데이터를 완전히 소유해야 해요. 원래 스코프가 끝난 뒤에 실행될 수도 있으니까요. 그렇게 하도록 강제하려면 move 키워드를 붙여요.
use std::thread;
fn main() { let message = String::from("스레드에서 보내는 인사");
let handle = thread::spawn(move || { println!("{message}"); });
handle.join().unwrap();}실행 결과:
스레드에서 보내는 인사move가 없으면 Rust는 이 코드를 컴파일하지 않아요. 새 스레드가 실제로 message를 쓸 때까지 message가 살아 있다고 보장할 수 없기 때문이에요. move는 message의 소유권을 클로저 안으로 옮겨요. 소유권 페이지에서 본 바로 그 “이동”이에요. 다른 변수 대신 스레드에게 넘겨줄 뿐이에요.
스레드끼리 데이터를 안전하게 공유하기
섹션 제목: “스레드끼리 데이터를 안전하게 공유하기”일반 참조자는 스레드를 넘나들 수 없어요(자신을 만든 스코프보다 오래 살 수 없으니까요). 그래서 스레드끼리 데이터를 공유할 때는 이미 만나 본 두 가지를 함께 써요. 공유 소유권을 위한 Arc(스마트 포인터 페이지에서 봤어요)와, 한 번에 한 스레드만 데이터를 건드리도록 보장하는 Mutex예요.
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![];
for _ in 0..5 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); }
for handle in handles { handle.join().unwrap(); }
println!("결과: {}", *counter.lock().unwrap());}실행 결과:
결과: 5counter.lock()은 값에 안전하게 단독으로 접근할 수 있을 때까지 기다렸다가, 값을 바꾸고 나면 자동으로 잠금을 풀어요. 다섯 스레드가 같은 카운터를 1씩 올리지만, Mutex 덕분에 서로 방해하지 않아요. 업데이트가 사라져서 더 작은 숫자가 나오는 일 없이, 언제나 5가 나와요.
공유 데이터를 잠그는 건 스레드가 협력하는 한 가지 방법이에요. 또 다른 흔한 방법은 아예 공유하지 않고, 스레드끼리 메시지를 보내는 거예요. 그 이야기는 채널 페이지에서 다뤄요.
Rust가 이걸 “겁 없는 동시성”이라고 부르는 이유
섹션 제목: “Rust가 이걸 “겁 없는 동시성”이라고 부르는 이유”대부분의 언어에서는 Mutex를 잠그는 걸 깜빡하거나, 공유하면 안 되는 참조자를 실수로 여러 스레드에 넘겨도 아무도 막아 주지 않아요. Rust 컴파일러는 이걸 실제로 검사해요. 스레드 사이에서 데이터 경쟁(data race)을 일으킬 코드는 아예 컴파일되지 않아요. 이 강좌에서 앞서 배운 소유권과 대여 시스템이 그대로, 하나의 스코프 대신 스레드에 적용되는 거예요.
연습 문제
섹션 제목: “연습 문제”1. 두 스레드로 하나의 합 구하기
섹션 제목: “1. 두 스레드로 하나의 합 구하기”두 스레드를 써서 1부터 100까지 더해 보세요. 한 스레드는 1부터 50까지, 다른 스레드는 51부터 100까지 더해요. 그다음 main이 두 답을 더해요. 힌트: 스레드의 클로저는 값을 반환할 수 있고, handle.join().unwrap()이 그 값을 돌려줘요.
정답 보기
use std::thread;
fn main() { let first = thread::spawn(|| { let total: u32 = (1..=50).sum(); total }); let second = thread::spawn(|| { let total: u32 = (51..=100).sum(); total });
let a = first.join().unwrap(); let b = second.join().unwrap(); println!("{a} + {b} = {}", a + b);}실행 결과:
1275 + 3775 = 5050이 클로저들은 바깥 변수를 하나도 쓰지 않아서 move가 필요 없어요. 두 스레드는 동시에 실행되고, join()은 각 스레드를 기다렸다가 결과를 돌려줘요.
2. 모두가 방명록에 이름 남기기
섹션 제목: “2. 모두가 방명록에 이름 남기기”스레드 세 개를 시작하세요. 각 스레드는 공유하는 Vec<String> 하나에 "2번 스레드 다녀감" 같은 줄을 추가해요. 모든 스레드가 끝나면 줄들을 정렬해서 출력하세요. 힌트: 카운터 예제처럼 Arc<Mutex<Vec<String>>>를 쓰고, 마지막에 복사본을 정렬하세요. 스레드가 어떤 순서로 끝날지 모르니까요.
정답 보기
use std::sync::{Arc, Mutex};use std::thread;
fn main() { let book = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![];
for id in 1..=3 { let book = Arc::clone(&book); handles.push(thread::spawn(move || { book.lock().unwrap().push(format!("{id}번 스레드 다녀감")); })); }
for handle in handles { handle.join().unwrap(); }
let mut lines = book.lock().unwrap().clone(); lines.sort(); for line in lines { println!("{line}"); }}실행 결과:
1번 스레드 다녀감2번 스레드 다녀감3번 스레드 다녀감lines.sort() 줄을 지우고 몇 번 실행해 보세요. 순서가 바뀔 수 있어요. 매번 같은 결과를 얻으려면 정렬해야 해요.
thread::spawn(closure)는 클로저를 새 스레드에서 실행해요..join()은 그 스레드가 끝날 때까지 기다려요.- 스레드에 넘기는 클로저는 보통
move를 붙여서, 자신이 쓰는 데이터의 소유권을 완전히 가져가야 해요. - 스레드끼리 데이터를 공유할 때는
Arc(공유 소유권)와Mutex(단독 접근)를 함께 써요.counter.lock()으로 한 번에 한 스레드씩 안전하게 값에 접근해요. - 데이터 경쟁을 일으킬 코드는 실행할 때가 아니라 컴파일할 때 컴파일러가 거부해요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. thread::spawn에 넘기는 클로저에 보통 move 키워드가 필요한 이유는 무엇일까요?
2. Arc<Mutex<T>>는 어디에 쓸까요?
3. 스레드 핸들에 .join()을 호출하는 걸 잊으면 어떤 문제가 생길 수 있을까요?
4. 같은 프로그램인데도 "메인:" 줄과 "스레드:" 줄의 순서가 실행할 때마다 달라지는 이유는 무엇일까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”- 스레드를 이용하여 코드를 동시에 실행하기 — The Rust Programming Language 한국어판:
spawn,join,move클로저를 다루고, 출력이 어떻게 섞이는지 예제로 보여 줘요. - 공유 상태 동시성 — The Rust Programming Language 한국어판:
Mutex와Arc를 깊이 다루고,Rc를 다른 스레드로 보낼 수 없는 이유도 설명해요. std::thread— 표준 라이브러리 문서 (영어): 스레드 이름 붙이기, 스코프 스레드(scoped thread)까지 스레드에 관한 모든 것이 있어요.Mutex— 표준 라이브러리 문서 (영어): 잠금이 어떻게 동작하는지, 그리고 잠금을 쥔 스레드가 패닉하면 생기는 “포이즈닝(poisoning)“이 무엇인지 설명해요.- Threads — Rust by Example (영어): 스레드 여러 개를 만들고 join하는 짧은 예제를 실행해 볼 수 있어요.