Arc/Mutex로 상태 공유하기
안전한 데이터 공유
Arc/Mutex로 상태 공유하기은(는) CoddyKit의 무료 Learn Rust Coding 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Learn Rust Coding 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Learn Rust Coding 강의에는 총 4개의 강의가 포함되어 있습니다.
공유 상태가 어려운 이유
때로는 메시지 전달만으로 충분하지 않아 여러 스레드가 실제로 같은 데이터를 읽고 써야 합니다. 보호 장치 없이 가변 값을 스레드 간에 공유하면 데이터 경쟁이 발생할 수 있으므로 Rust에서는 이를 허용하지 않습니다.
함께 사용하는 두 도구는 다음과 같습니다.
- 스레드 간 공유 소유권을 위한
Arc - 안전하고 배타적인 변경을 위한
Mutex
Rc는 스레드 안전하지 않습니다
Rc는 공유 소유권을 제공하지만 하나의 스레드에서만 사용할 수 있습니다. 참조 횟수가 동기화되지 않으므로 컴파일러는 이를 스레드 간에 전달하지 못하게 합니다. 여러 스레드에서 공유하려면 Arc(원자적으로 참조 횟수를 세는 포인터)가 필요합니다.
Arc는 Rc와 비슷하게 동작하지만 원자적 연산으로 횟수를 갱신하므로 스레드 간 복제가 안전합니다.
use std::sync::Arc;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let clone1 = Arc::clone(&data);
println!("original: {:?}", data);
println!("clone: {:?}", clone1);
println!("count: {}", Arc::strong_count(&data));
}Mutex는 배타적 접근을 제공합니다
Mutex는 데이터를 감싸고 한 번에 하나의 스레드만 데이터에 접근하도록 보장합니다. .lock()을 호출하면 내부 값을 역참조하는 MutexGuard를 얻습니다. 가드가 범위를 벗어나면 잠금이 자동으로 해제됩니다.
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num += 10;
} // guard dropped here, lock released
println!("value = {:?}", m.lock().unwrap());
}Arc와 Mutex 결합하기
가변 데이터를 스레드 간에 공유하려면 다음과 같이 감쌉니다: Arc
Arc를 사용하면 여러 스레드가 같은 데이터를 가리키는 핸들을 소유할 수 있습니다.Mutex를 사용하면 각 스레드가 한 번에 하나씩 안전하게 데이터를 변경할 수 있습니다.
스레드를 생성하기 전에 각 스레드에 사용할 Arc를 복제합니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
});
handle.join().unwrap();
println!("counter = {}", *counter.lock().unwrap());
}여러 스레드에서 공유하는 카운터
대표적인 예는 열 개의 스레드가 공유 카운터를 각각 증가시키는 경우입니다. 각 스레드는 자신의 Arc 복사본을 보유하고 Mutex를 잠근 뒤 1을 더합니다. 모든 스레드를 결합하고 나면 데이터 경쟁 없이 합계가 정확히 10이 됩니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}잠금 범위가 중요합니다
MutexGuard는 삭제될 때까지 잠금을 유지합니다. 느린 작업을 수행하는 동안 가드를 유지하면 다른 스레드가 차단됩니다. 임계 구역은 짧게 유지하세요. 잠그고, 변경하고, 해제하면 됩니다. 작은 블록으로 잠금 범위를 감싸면 추가 처리를 시작하기 전에 잠금이 해제됩니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let log = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for i in 0..3 {
let l = Arc::clone(&log);
handles.push(thread::spawn(move || {
{
let mut v = l.lock().unwrap();
v.push(i);
} // released quickly
}));
}
for h in handles { h.join().unwrap(); }
let mut result = log.lock().unwrap().clone();
result.sort();
println!("{:?}", result);
}교착 상태: 실제 위험
교착 상태는 두 스레드가 서로 상대방이 필요로 하는 잠금을 보유한 채 영원히 기다릴 때 발생합니다. Rust는 데이터 경쟁을 막지만 교착 상태까지 막지는 않습니다. 여러 뮤텍스를 항상 같은 순서로 잠그고 잠금 유지 시간을 짧게 하면 교착 상태를 피할 수 있습니다.
또한 한 스레드에서 같은 Mutex를 두 번 잠그지 마세요. 표준 Mutex는 재진입을 지원하지 않습니다.
스레드 패닉 발생 시 오염
잠금을 보유한 스레드에서 패닉이 발생하면 Mutex가 오염된 상태가 됩니다. 이후 .lock() 호출은 데이터가 일관되지 않을 수 있음을 알리기 위해 Err를 반환합니다. 데이터가 여전히 사용 가능하다고 판단한다면 오류에 대해 into_inner()를 호출하여 내부 가드를 복구할 수 있습니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let d = Arc::clone(&data);
let _ = thread::spawn(move || {
let mut g = d.lock().unwrap();
*g = 7;
panic!("boom"); // poisons the mutex
}).join();
match data.lock() {
Ok(g) => println!("ok: {}", *g),
Err(poisoned) => println!("recovered: {}", *poisoned.into_inner()),
}
}많은 독자를 위한 RwLock
쓰기보다 읽기가 훨씬 많은 경우에는 RwLock이 Mutex보다 빠를 수 있습니다. 여러 독자가 동시에 읽거나 한 명의 작성자가 쓰는 것을 허용합니다. 공유 접근에는 .read()를 사용하고, 배타적 접근에는 .write()를 사용합니다.
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let config = Arc::new(RwLock::new(String::from("v1")));
let reader = Arc::clone(&config);
let r = thread::spawn(move || {
let val = reader.read().unwrap();
println!("read: {}", *val);
});
r.join().unwrap();
{
let mut w = config.write().unwrap();
*w = String::from("v2");
}
println!("final: {}", *config.read().unwrap());
}간단한 카운터에는 원자 타입 사용하기
정수 카운터 하나만 필요하다면 완전한 Mutex는 지나치게 복잡합니다. AtomicUsize와 같은 타입은 fetch_add 같은 메서드를 통해 잠금 없는 갱신을 제공합니다. 스레드 간에 공유하려면 이를 Arc로 감싸세요. Ordering을 선택해야 하며, SeqCst가 가장 간단하고 안전한 기본값입니다.
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
fn main() {
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
c.fetch_add(1, Ordering::SeqCst);
}));
}
for h in handles { h.join().unwrap(); }
println!("count = {}", counter.load(Ordering::SeqCst));
}적절한 도구 선택하기
공유 상태를 위한 간단한 지침은 다음과 같습니다.
- Channel: 소유권을 전달하며, 파이프라인 방식으로 동작합니다.
- Arc<Mutex>: 읽기와 쓰기가 섞인 공유 가변 구조에 사용합니다.
- Arc<RwLock>: 읽기가 많은 공유 데이터에 사용합니다.
- Atomics: 단일 기본 타입의 카운터나 플래그에 사용합니다.
문제에 맞는 가장 간단한 도구를 우선 사용하세요. 메시지 전달로 문제를 적절히 모델링하기 어려울 때만 잠금을 사용하면 됩니다.
빠른 확인
공유 상태에 대한 이해도를 확인해 보세요.
복습
스레드 간에 상태를 안전하게 공유하는 방법을 배웠습니다.
Arc는 스레드 안전한 공유 소유권을 제공하지만Rc는 제공하지 않습니다.Mutex는 자동으로 잠금이 해제되는 가드를 통해 배타적인 변경을 제공합니다.Arc<Mutex<T>>는 공유 가변 데이터에 사용하는 표준 패턴입니다.- 잠금 범위를 짧게 유지하고 교착 상태와 오염에 주의하세요.
RwLock은 읽기가 많은 데이터에 적합하고, 원자 타입은 간단한 카운터에 적합합니다.
자주 묻는 질문
“Arc/Mutex로 상태 공유하기” 강의는 무료인가요?
네 — “Arc/Mutex로 상태 공유하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Learn Rust Coding 강의 전체를 잠금 해제할 수 있습니다. Learn Rust Coding 강의에는 총 4개의 강의가 포함되어 있습니다.
“Arc/Mutex로 상태 공유하기”에서 뭘 배우나요?
안전한 데이터 공유 브라우저에서 직접 실행하는 실습 코드로 Learn Rust Coding을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Learn Rust Coding을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Learn Rust Coding은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“Arc/Mutex로 상태 공유하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Learn Rust Coding 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Learn Rust Coding 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- mpsc 채널
- Arc/Mutex로 상태 공유하기
- 범위가 지정된 스레드
- Crossbeam 채널