콘텐츠로 이동

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());
}

실행 결과:

결과: 5
▶ 직접 실행해 보기

counter.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