콘텐츠로 이동

Rust 공유 상태: RwLock, 원자적 타입, 스코프 스레드

동시성 페이지에서는 Arc<Mutex<T>>로 스레드끼리 카운터를 공유했어요. Arc는 여러 스레드가 값을 함께 소유하게 해 주고, Mutex는 한 번에 한 스레드만 값을 건드리게 해 줘요. 채널 페이지에서는 메시지를 보내서 공유 자체를 피했고요. 둘 다 훌륭한 도구라서, 이것만으로도 거의 모든 걸 만들 수 있어요.

그런데 Mutex는 사무실 화장실 열쇠가 딱 하나뿐인 것과 비슷해요. 잘 동작하긴 하지만, 복도 게시판을 읽고 싶을 뿐인데 열쇠를 받아야 한다면 지나치죠. Rust 표준 라이브러리에는 각자 특정한 일에 더 잘 맞는 도구가 몇 가지 더 있어요. 이 페이지에서는 그중 네 가지를 만나 볼 거예요. 스코프 스레드(scoped thread), RwLock, 원자적 타입(atomic), 그리고 OnceLock/LazyLock이에요. 컴파일러가 대신 잡아 주지 못하는 유일한 버그, 데드락(deadlock)을 피하는 방법도 배워요.

스코프 스레드: 이동 대신 대여하기

섹션 제목: “스코프 스레드: 이동 대신 대여하기”

thread::spawn을 쓰면 보통 move가 필요하고, Arc도 자주 필요해요. 새로 만든 스레드는 main이 다음 일로 넘어간 뒤에도 계속 실행될 수 있어서, Rust가 지역 변수를 빌려 쓰지 못하게 하거든요.

std::thread::scope는 이 문제를 하나의 약속으로 해결해요. 스코프 안에서 시작한 스레드는 모두 스코프가 끝나기 전에 끝나요. 스레드가 데이터보다 오래 살 수 없으니, 그냥 빌려 써도 괜찮아요. Arc도, move도, 복제도 필요 없어요.

use std::thread;
fn main() {
let shop = "동네 가게";
let sales = vec![12, 7, 30, 5, 18, 22, 9, 14];
let (morning, afternoon) = sales.split_at(4);
let (am, pm) = thread::scope(|s| {
let am = s.spawn(|| morning.iter().sum::<i32>());
let pm = s.spawn(|| afternoon.iter().sum::<i32>());
(am.join().unwrap(), pm.join().unwrap())
});
println!("{shop}: 오전 {am}, 오후 {pm}, 하루 합계 {}", am + pm);
println!("여전히 main 소유: {:?}", sales);
}

실행 결과:

동네 가게: 오전 54, 오후 63, 하루 합계 117
여전히 main 소유: [12, 7, 30, 5, 18, 22, 9, 14]
▶ 직접 실행해 보기

무슨 일이 일어나는지 볼게요.

  • thread::scope(|s| { ... })로 스코프를 열어요. 스레드는 thread::spawn(...) 대신 s.spawn(...)으로 시작해요.
  • 두 클로저는 morning과 afternoon을 빌려 써요. 둘 다 sales의 슬라이스일 뿐이라서, 아무것도 복사되지 않아요.
  • 스코프 클로저가 반환하는 값(여기서는 합계 두 개를 담은 튜플)이 thread::scope의 값이 돼요.
  • 스코프가 끝난 뒤에도 sales는 여전히 main의 것이라서 계속 쓸 수 있어요.

join() 호출을 깜빡해도, 스코프는 닫는 }에서 모든 스레드를 기다려요. 바로 그 약속 덕분에 빌려 쓰는 게 안전한 거예요.

RwLock: 여러 명이 읽거나, 한 명이 쓰거나

섹션 제목: “RwLock: 여러 명이 읽거나, 한 명이 쓰거나”

카페의 메뉴판을 떠올려 보세요. 손님 여러 명이 동시에 읽어도 아무 문제가 없어요. 하지만 사장님이 가격을 바꿀 때는 새 메뉴판이 걸릴 때까지 모두 잠깐 기다려야 해요.

이게 바로 RwLock(“읽기-쓰기 잠금”, read-write lock)이에요. 들어가는 방법이 두 가지 있어요.

  • read()는 공유 읽기 가드를 줘요. 여러 스레드가 동시에 가질 수 있어요.
  • write()는 배타적인 쓰기 가드를 줘요. 누군가 쓰기 가드를 쥐고 있는 동안에는 다른 누구도 읽거나 쓸 수 없어요.

Mutex는 이런 구분을 하지 않아요. 읽기만 하는 두 스레드도 차례를 기다려야 해요. 아래는 요청 처리기 여러 개가 읽고, 관리자가 한 번 바꾸는 설정 캐시예요.

use std::sync::{Arc, RwLock};
use std::thread;
struct Settings {
theme: String,
max_upload_mb: u32,
}
fn main() {
let settings = Arc::new(RwLock::new(Settings {
theme: String::from("light"),
max_upload_mb: 10,
}));
// 요청 처리기 4개가 동시에 설정을 읽어요.
let mut handles = Vec::new();
for id in 1..=4 {
let settings = Arc::clone(&settings);
handles.push(thread::spawn(move || {
let s = settings.read().unwrap(); // 공유: 다른 스레드도 읽을 수 있어요
format!("요청 {id}: 테마={}, 제한={}MB", s.theme, s.max_upload_mb)
}));
}
for handle in handles {
println!("{}", handle.join().unwrap());
}
// 관리자가 설정을 한 번 바꿔요.
{
let mut s = settings.write().unwrap(); // 배타적: 나머지는 모두 기다려요
s.theme = String::from("dark");
s.max_upload_mb = 25;
} // 여기서 쓰기 가드가 버려져서 잠금이 다시 풀려요
let s = settings.read().unwrap();
println!("변경 후: 테마={}, 제한={}MB", s.theme, s.max_upload_mb);
}

실행 결과:

요청 1: 테마=light, 제한=10MB
요청 2: 테마=light, 제한=10MB
요청 3: 테마=light, 제한=10MB
요청 4: 테마=light, 제한=10MB
변경 후: 테마=dark, 제한=25MB
▶ 직접 실행해 보기

스레드는 어떤 순서로든 끝날 수 있지만, 출력은 항상 같아요. 각 스레드가 줄을 직접 출력하지 않고 반환하고, main이 join하는 순서대로 출력하거든요.

RwLock이 Mutex보다 나은 때는 언제일까요? 읽기는 잦고 쓰기는 드물며, 읽기 한 번에 시간이 조금 걸릴 때예요(예를 들어 큰 캐시를 훑어볼 때). 그러면 읽는 스레드들이 정말로 나란히 실행돼요.

그렇지 않은 때는요? 읽기가 아주 짧거나 쓰기가 계속 일어난다면, RwLock은 그냥 더 복잡한 Mutex일 뿐이에요. 관리 작업은 더 많이 하는데, 어차피 읽는 쪽과 쓰는 쪽이 서로를 기다리느라 바빠요. 잘 모르겠으면 Mutex로 시작하세요. 더 단순하고, 나중에 측정해 보니 잠금이 병목이라면 그때 바꾸면 돼요.

숫자 하나나 참/거짓 플래그 하나라면, 잠금조차 과해요. AtomicUsize, AtomicI64, AtomicBool 같은 원자적(atomic) 타입은 잠금 없이도 여러 스레드가 동시에 안전하게 바꿀 수 있어요. CPU가 각 연산을 쪼갤 수 없는 한 단계로 처리하기 때문에, 갱신이 사라지는 일이 없어요.

여기서는 스레드 8개가 각각 페이지 조회를 1000번 기록해요. 투표도 세는데, 찬성 한 번, 반대 세 번을 계속 반복해요. 투표 합계가 음수가 되기 때문에 부호 없는 타입이 아니라 AtomicI64를 써요.

use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::thread;
fn main() {
let page_views = AtomicUsize::new(0);
let net_votes = AtomicI64::new(0);
thread::scope(|s| {
for _ in 0..8 {
s.spawn(|| {
for n in 0..1000 {
page_views.fetch_add(1, Ordering::Relaxed);
if n % 4 == 0 {
net_votes.fetch_add(1, Ordering::Relaxed); // 찬성
} else {
net_votes.fetch_sub(1, Ordering::Relaxed); // 반대
}
}
});
}
}); // 여기서는 스레드 8개가 모두 끝났어요
println!("페이지 조회수: {}", page_views.load(Ordering::Relaxed));
println!("투표 합계: {}", net_votes.load(Ordering::Relaxed));
}

실행 결과:

페이지 조회수: 8000
투표 합계: -4000
▶ 직접 실행해 보기

page_views가 mut이 아니라는 점을 눈여겨보세요. 원자적 타입은 공유 참조를 통해서 값이 바뀌어요. 바로 그래서 여러 스레드가 동시에 쓸 수 있는 거예요. 가장 많이 쓰는 메서드는 이래요.

  • fetch_add(n, ordering) / fetch_sub(n, ordering)은 더하거나 빼고, 이전 값을 돌려줘요.
  • load(ordering)은 현재 값을 읽어요.
  • store(value, ordering)은 값을 덮어써요.

원자적 연산은 모두 Ordering을 요구해요. 이 연산이 주변의 메모리 연산과 얼마나 엄격하게 순서를 지켜야 하는지 CPU와 컴파일러에게 알려 주는 값이에요. 깊이 들어가면 꽤 어려운 주제지만, 입문자라면 규칙 두 가지만 알면 돼요.

  • 잘 모르겠으면 Ordering::SeqCst를 쓰세요. 가장 엄격하고 가장 안전한 선택이에요.
  • 마지막에 한 번만 읽는 단순한 카운터라면 Ordering::Relaxed로 충분해요. 위의 예제처럼요. 합계는 여전히 정확해요.

언젠가 더 깊이 알고 싶다면, 무료 책 Rust Atomics and Locks(맨 아래에 링크가 있어요)가 ordering을 알기 쉽게 설명해 줘요.

AtomicBool은 “이제 멈춰 줘” 신호로 딱 좋아요. 여기서는 백그라운드 작업 스레드가 main이 플래그를 바꿀 때까지 자동 저장을 계속해요.

use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
fn main() {
let stop = AtomicBool::new(false);
let report = thread::scope(|s| {
let worker = s.spawn(|| {
while !stop.load(Ordering::SeqCst) {
// 여기서 문서를 자동 저장한다고 생각해 보세요
thread::sleep(Duration::from_millis(5));
}
"자동 저장 작업 스레드가 깔끔하게 멈췄어요"
});
thread::sleep(Duration::from_millis(30)); // main은 다른 일을 해요
stop.store(true, Ordering::SeqCst); // 작업 스레드에게 멈춰 달라고 해요
worker.join().unwrap()
});
println!("{report}");
}

실행 결과:

자동 저장 작업 스레드가 깔끔하게 멈췄어요
▶ 직접 실행해 보기

작업 스레드는 반복문을 한 바퀴 돌 때마다 플래그를 확인해요. 지금 하던 단계를 마친 다음 스스로 빠져나가요. 파일을 저장하는 도중에 스레드를 강제로 죽이는 것보다 훨씬 친절한 방법이에요.

OnceLock과 LazyLock: 한 번만 준비하기

섹션 제목: “OnceLock과 LazyLock: 한 번만 준비하기”

모든 스레드가 읽을 수 있는 전역 값이 하나 필요할 때가 있어요. 조회용 표, 앱 이름, 시작할 때 불러오는 설정 같은 것들이요. 까다로운 건 그 값을 만드는 부분이에요. 두 스레드가 동시에 만들려고 하면 어떻게 될까요?

std::sync::LazyLock과 std::sync::OnceLock이 이 문제를 해결해요. 값은 누군가 처음 필요로 할 때 정확히 한 번 만들어져요. 만드는 도중에 다른 스레드가 요청하면 그냥 기다렸다가, 모두가 같은 값을 함께 써요.

use std::collections::HashMap;
use std::sync::{LazyLock, OnceLock};
use std::thread;
// LazyLock: 만드는 방법을 여기에 바로 적어 두고, 처음 쓸 때 실행돼요.
static CURRENCIES: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| {
println!("통화 표를 만드는 중...");
HashMap::from([("US", "USD"), ("JP", "JPY"), ("DE", "EUR"), ("BR", "BRL")])
});
// OnceLock: 비어 있는 상태로 시작하고, 채우는 방법은 쓰는 곳에서 정해요.
static STORE_NAME: OnceLock<String> = OnceLock::new();
fn store_name() -> &'static str {
STORE_NAME.get_or_init(|| {
std::env::var("STORE_NAME").unwrap_or_else(|_| String::from("러스티 상점"))
})
}
fn main() {
let mut handles = Vec::new();
for country in ["US", "JP", "DE", "BR"] {
handles.push(thread::spawn(move || {
format!("{}의 {country} 매장은 {}로 결제해요", store_name(), CURRENCIES[country])
}));
}
for handle in handles {
println!("{}", handle.join().unwrap());
}
}

실행 결과:

통화 표를 만드는 중...
러스티 상점의 US 매장은 USD로 결제해요
러스티 상점의 JP 매장은 JPY로 결제해요
러스티 상점의 DE 매장은 EUR로 결제해요
러스티 상점의 BR 매장은 BRL로 결제해요
▶ 직접 실행해 보기

스레드 4개가 거의 동시에 표를 원하는데도 “통화 표를 만드는 중…“은 딱 한 번만 출력돼요. 그리고 이 값들은 프로그램이 끝날 때까지 살아 있는 static 값이라서, 스레드들은 Arc가 전혀 필요 없어요.

어느 쪽을 골라야 할까요?

  • LazyLock: 값을 선언하는 자리에서 만드는 방법을 알고 있을 때 써요. 평범한 전역 변수처럼 느껴져요.
  • OnceLock: 값이 나중에 정해질 때 써요. 예를 들어 명령줄 플래그로 정해지는 경우요. get_or_init(...)이나 set(...)으로 채워요(set은 이미 채워져 있으면 얌전히 실패해요).

둘 다 표준 라이브러리에 있어요. LazyLock은 Rust 1.80에 들어왔기 때문에, 오래된 코드에서는 같은 일을 once_cell이나 lazy_static 크레이트로 하는 경우가 많아요.

컴파일러는 데이터 경합(data race)을 막아 주지만, 잡아내지 못하는 동시성 버그가 하나 있어요. 바로 데드락(deadlock)이에요. 스레드들이 서로를 영원히 기다려서 프로그램이 그냥 멈춰 버리는 상황이에요. 에러도, 크래시도 없이 아무 일도 일어나지 않아요.

전형적인 원인은 잠금 두 개를 서로 다른 순서로 잡는 거예요. 두 사람이 모두 펜과 공책이 둘 다 필요하다고 해 볼게요. 한 사람은 펜을 집고, 다른 사람은 공책을 집어요. 그리고 서로 상대가 내려놓기를 예의 바르게 기다려요. 영원히요.

// 스레드 A // 스레드 B
let from = alice.lock().unwrap(); let from = bob.lock().unwrap();
let to = bob.lock().unwrap(); let to = alice.lock().unwrap();
// A는 alice를 쥐고 bob을 기다려요. // B는 bob을 쥐고 alice를 기다려요. 멈춤!

안전하게 지내는 습관 세 가지가 있어요.

  1. 잠금은 항상 같은 순서로 잡으세요. 모든 스레드가 bob보다 alice를 먼저 잠그면(예를 들어 계좌 ID 순으로), 서로 물고 물리는 순환이 생길 수 없어요.
  2. 잠금 범위를 짧게 유지하세요. 데이터를 읽거나 바꾸는 데 필요한 만큼만 잠금을 쥐세요. 네트워크 호출, 파일 쓰기, 긴 계산처럼 느린 작업을 하는 동안에는 절대 쥐고 있지 마세요. 그동안 다른 스레드는 모두 기다리느라 꼼짝 못 해요.
  3. 가드는 일찍 버리세요. 가드는 버려질 때(drop) 잠금을 풀어요. 위의 RwLock 쓰기 예제처럼 안쪽 블록 { ... }으로 끝내거나, 직접 drop(guard)를 호출하면 돼요.

습관 3을 실제로 볼게요. 각 작업 스레드는 공유 큐에서 작업을 하나 꺼내고, 큐의 잠금을 푼 다음에야 느린 부분을 처리해요.

use std::sync::Mutex;
use std::thread;
fn main() {
let queue = Mutex::new(vec!["사진 크기 조정", "이메일 보내기", "보고서 만들기"]);
let finished = Mutex::new(Vec::new());
thread::scope(|s| {
for _ in 0..3 {
s.spawn(|| {
let mut jobs = queue.lock().unwrap();
let job = jobs.pop();
drop(jobs); // 느린 부분 전에 큐 잠금을 풀어요
if let Some(job) = job {
let result = format!("완료: {job}"); // 이 부분이 몇 초 걸린다고 상상해 보세요
finished.lock().unwrap().push(result);
}
});
}
});
// 스코프가 끝났으니 이제 `finished`를 쓰는 스레드는 없어요.
let mut done = finished.into_inner().unwrap();
done.sort(); // 스레드는 어떤 순서로든 끝날 수 있어요
for line in done {
println!("{line}");
}
}

실행 결과:

완료: 보고서 만들기
완료: 사진 크기 조정
완료: 이메일 보내기
▶ 직접 실행해 보기

drop(jobs)가 없으면 가드는 클로저가 끝날 때까지 살아 있어요. 그러면 “느린” 작업이 큐가 잠긴 상태에서 실행되고, 작업 스레드 세 개가 나란히 일하지 못하고 하나씩 차례로 일하게 돼요.

알아 둘 만한 교묘한 경우가 하나 있어요. while let Some(job) = queue.lock().unwrap().pop() { ... }에서는 가드가 반복문 본문 전체에 걸쳐 살아 있어요. 그래서 작업을 처리하는 내내 큐가 잠겨 있어요. 위 예제처럼 별도의 let 문으로 작업을 먼저 꺼내세요.

하고 싶은 일 쓸 도구
한 스레드에서 다른 스레드로 작업이나 결과 보내기 채널 (mpsc)
여러 스레드가 같은 값을 읽고 바꾸기 Arc<Mutex<T>>
자주 읽고 가끔 바뀌는 데이터 공유하기 Arc<RwLock<T>>
개수 세기, 또는 단순한 켜짐/꺼짐 플래그 공유하기 원자적 타입 (AtomicUsize, AtomicBool, …)
전역 값을 한 번 만들고 그 뒤로는 읽기만 하기 LazyLock 또는 OnceLock
Arc 없이 스레드가 지역 데이터를 빌려 쓰기 thread::scope

이 도구들은 자유롭게 섞어 쓸 수 있어요. 이 페이지의 예제에서도 이미 thread::scope를 원자적 타입과, 또 Mutex와 함께 썼어요. 상황에 맞는 가장 단순한 도구를 고르세요. “잘못” 고를까 봐 걱정하지 않아도 돼요. 나중에 바꾸는 건 보통 작은 수정이면 끝나요.

thread::spawn에서 지역 변수 빌리기. 별문제 없어 보이죠.

use std::thread;
fn main() {
let sales = vec![12, 7, 30, 5];
let handle = thread::spawn(|| {
let total: i32 = sales.iter().sum();
total
});
println!("합계: {}", handle.join().unwrap());
}
error[E0373]: closure may outlive the current function, but it borrows `sales`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `sales`
7 | let total: i32 = sales.iter().sum();
| ----- `sales` is borrowed here
|
help: to force the closure to take ownership of `sales` (and any other referenced variables), use the `move` keyword

컴파일러 메시지는 영어로 나와요. “클로저가 현재 함수보다 오래 살 수 있는데, 현재 함수가 소유한 sales를 빌리고 있다”는 뜻이에요. 여러분은 바로 join()을 호출할 걸 알지만, thread::spawn은 그걸 알 수 없어요. thread::spawn 입장에서는 스레드가 영원히 실행될 수도 있거든요. 해결 방법은 두 가지예요. move를 붙이거나(그러면 main은 그 뒤로 sales를 쓸 수 없어요), 빌려 쓸 수 있는 thread::scope를 쓰는 거예요.

let total: i32 = thread::scope(|s| s.spawn(|| sales.iter().sum()).join().unwrap());

Arc 빠뜨리기. Arc가 없으면 첫 번째 스레드의 move 클로저가 Mutex를 가져가 버려서, 다음 스레드에게는 아무것도 남지 않아요.

use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..3 {
handles.push(thread::spawn(move || {
*counter.lock().unwrap() += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
}
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..3 {
| ------------- inside of this loop
9 | handles.push(thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop

에러 메시지는 “반복문의 이전 회차에서 counter가 이미 클로저로 이동했다”는 뜻이에요. Arc::new(...)로 감싸고, 동시성 페이지에서처럼 반복문 맨 위에서 Arc를 복제하세요. 아니면 thread::scope를 쓰고 move를 빼세요. Arc 대신 Rc를 쓰면 error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely(“스레드 사이로 안전하게 보낼 수 없다”)라는 에러가 나와요. Rc는 한 스레드 전용이에요(스마트 포인터 참고).

1부터 1000까지의 숫자로 Vec을 만드세요. thread::scope를 써서 250개씩 4묶음으로 나누고, 각 묶음을 별도의 스레드에서 더하세요. 묶음별 합계와 총합을 출력하세요. 힌트: numbers.chunks(250)가 슬라이스들을 줘요. 핸들을 Vec에 모은 다음, 순서대로 join하세요.

정답 보기
use std::thread;
fn main() {
let numbers: Vec<u64> = (1..=1000).collect();
let sums: Vec<u64> = thread::scope(|s| {
let mut handles = Vec::new();
for chunk in numbers.chunks(250) {
handles.push(s.spawn(move || chunk.iter().sum::<u64>()));
}
handles.into_iter().map(|h| h.join().unwrap()).collect()
});
println!("묶음별 합계: {:?}", sums);
println!("총합: {}", sums.iter().sum::<u64>());
}

실행 결과:

묶음별 합계: [31375, 93875, 156375, 218875]
총합: 500500
▶ 직접 실행해 보기

여기서 move가 각 스레드로 옮기는 건 chunk뿐이에요. chunk는 빌려 온 작은 슬라이스예요. 벡터 자체는 이동하지 않고 빌려진 상태라서, 스코프가 끝난 뒤에도 numbers를 다시 쓸 수 있어요.

온라인 쇼핑몰이 주문마다 1000부터 시작하는 번호를 붙여요. 스레드 4개를 시작하고, 각 스레드가 공유 AtomicU64 하나에서 주문 번호를 3개씩 받게 하세요. 번호 12개를 모두 모아 정렬한 뒤 출력하세요. 힌트: fetch_add(1, Ordering::SeqCst)는 더하기 전의 값을 반환하니까, 각 번호를 정확히 한 번씩만 나눠 줘요.

정답 보기
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
fn main() {
let next_order = AtomicU64::new(1000);
let mut numbers: Vec<u64> = thread::scope(|s| {
let mut handles = Vec::new();
for _ in 0..4 {
handles.push(s.spawn(|| {
(0..3)
.map(|_| next_order.fetch_add(1, Ordering::SeqCst))
.collect::<Vec<u64>>()
}));
}
handles.into_iter().flat_map(|h| h.join().unwrap()).collect()
});
numbers.sort();
println!("주문 {}건: {:?}", numbers.len(), numbers);
}

실행 결과:

주문 12건: [1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011]
▶ 직접 실행해 보기

어떤 스레드가 어떤 번호를 받는지는 실행할 때마다 달라지지만, 같은 번호가 두 번 나가거나 건너뛰는 일은 절대 없어요. 정렬했기 때문에 출력되는 목록은 매번 같아요.

  • thread::scope는 안에서 만든 스레드가 스코프가 끝나기 전에 모두 끝난다고 보장해요. 그래서 스레드가 Arc나 move 없이 지역 데이터를 빌려 쓸 수 있어요.
  • RwLock은 여러 명이 읽거나 또는 한 명이 쓰게 해 줘요. 읽기가 많은 데이터에 도움이 되고, 그렇지 않다면 Mutex가 더 단순해요.
  • 원자적 타입(AtomicUsize, AtomicI64, AtomicBool)은 잠금 없이 카운터와 플래그를 다뤄요. 잘 모르겠으면 SeqCst, 단순한 카운터라면 Relaxed를 쓰세요.
  • LazyLock과 OnceLock은 여러 스레드가 동시에 요청해도 전역 값을 안전하게 정확히 한 번만 만들어요.
  • 데드락을 피하려면 잠금을 항상 같은 순서로 잡고, 잠금 범위를 짧게 유지하고, 가드를 일찍 버리세요.

이해도 점검

1. thread::scope 안에서 s.spawn으로 시작한 스레드가 지역 변수를 빌려 쓸 수 있는 이유는 무엇일까요?

2. RwLock에 대한 설명으로 옳은 것은 무엇일까요?

3. 여러 스레드가 공유하는 페이지 조회수 카운터가 필요해요. 가장 단순하면서 좋은 선택은 무엇일까요?

4. 스레드 A는 alice, bob 순서로 잠그고, 스레드 B는 bob, alice 순서로 잠가요. 어떤 일이 생길 수 있을까요?

점수: 0 / 4