Arc와 Mutex: Rust의 데이터 동기화와 공유 도구
Rust에서 동시성 프로그래밍을 구현할 때 Arc와 Mutex는 매우 중요한 역할을 합니다. 두 도구는 서로 보완적인 관계로, 다중 스레드 환경에서 안전하게 데이터를 공유하고 동기화하는 데 사용됩니다. 이 글에서는 Arc와 Mutex의 특징, 사용법, 그리고 기존 스마트 포인터와의 차이점에 대해 자세히 설명합니다. 또한, 실제 자주 합성해서 사용하는 사례도 함께 다룹니다.
기본적인 동시성에 대한 개념이 필요하면 아래 글을 꼭 읽어보세요.
[Rust/Concepts] - [Rust] 동시성 프로그래밍: thread, Mutex, Arc 사용 예제
1. Arc란?
Arc(Atomic Reference Counting)는 다중 스레드 환경에서 데이터를 안전하게 공유하기 위한 스마트 포인터입니다.
1.1. 주요 특징
- 참조 카운팅:
Rc
와 마찬가지로 데이터를 참조하는 포인터의 개수를 관리합니다. - 원자성 보장:
Arc
는 다중 스레드에서 안전하게 작동하도록 원자적으로 참조 카운팅을 업데이트합니다. - 읽기 전용 공유: 스레드 간에 데이터를 공유할 수 있지만, 데이터 자체를 수정하려면 추가 도구(예:
Mutex
)가 필요합니다.
1.2. Arc 사용 예제
use std::sync::Arc;
use std::thread;
fn main() {
let numbers = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for i in 0..5 {
let numbers = Arc::clone(&numbers);
handles.push(thread::spawn(move || {
println!("Thread {}: {:?}", i, numbers);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
Arc::clone
:Arc
는 데이터의 소유권을 복사하지 않고 참조를 증가시킵니다.- 참조된 데이터는 읽기 전용으로 안전하게 접근할 수 있습니다.
2. Mutex란?
Mutex는 **상호 배제(Mutual Exclusion)**를 제공하는 동기화 도구로, 다중 스레드가 동일한 데이터에 동시에 접근하지 못하도록 보장합니다.
2.1. 주요 특징
- 데이터 보호: 한 번에 한 스레드만 데이터에 접근 가능.
- 락(lock) 메커니즘: 데이터를 사용하려면
lock()
을 호출하여 잠금을 확보해야 함. - 데드락 주의: 잘못된 사용으로 데드락이 발생할 수 있으므로 적절히 관리해야 함.
2.2. Mutex 사용 예제
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(0);
{
let mut num = counter.lock().unwrap();
*num += 1;
}
println!("Counter: {:?}", counter);
}
lock()
은 잠금을 반환하며, 이 잠금을 통해 데이터에 접근할 수 있습니다.- 잠금이 해제되면 다른 스레드가 데이터에 접근할 수 있습니다.
3. Arc와 Mutex의 결합
Arc와 Mutex를 함께 사용하면 다중 스레드 환경에서 읽기-쓰기 공유가 가능합니다.
예제: 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..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("최종 카운터 값: {}", *counter.lock().unwrap());
}
- Arc는 데이터를 다중 스레드에서 안전하게 공유합니다.
- Mutex는 데이터를 보호하며, 하나의 스레드만 접근할 수 있도록 보장합니다.
Arc
와Mutex
의 조합은 읽기-쓰기 작업이 필요한 동시성 프로그래밍에서 자주 사용됩니다.
4. Arc와 Mutex vs 다른 스마트 포인터 비교
특징 | Rc | Arc | Mutex | Arc + Mutex |
---|---|---|---|---|
참조 카운팅 | 단일 스레드만 가능 | 다중 스레드에서 원자성 보장 | 참조 카운팅 없음 | 다중 스레드에서 참조와 데이터 보호 |
스레드 안전성 | 비동기 환경에 적합하지 않음 | 스레드 간 공유 가능 | 스레드 안전성 없음 | 스레드 간 공유 및 동기화 가능 |
데이터 변경 가능 여부 | 불가능 | 불가능 | 가능 | 가능 |
사용 사례 | 단일 스레드 데이터 공유 | 다중 스레드 데이터 읽기 전용 | 단일 스레드 동기화 | 다중 스레드에서 읽기-쓰기 공유 |
5. Arc와 Mutex를 활용한 실제 사례
5.1 공유 상태를 가진 스레드
다중 스레드가 동일한 상태를 업데이트하도록 설계할 수 있습니다.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![]));
let mut handles = vec![];
for i in 0..10 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut vec = data.lock().unwrap();
vec.push(i);
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("결과 데이터: {:?}", *data.lock().unwrap());
}
5.2 채널과 함께 사용하기
Rust의 채널과 함께 사용하면 효율적으로 데이터를 전달하고 처리할 수 있습니다.
use std::sync::{Arc, Mutex};
use std::sync::mpsc;
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(vec![]));
let (tx, rx) = mpsc::channel();
let producer = thread::spawn({
let tx = tx.clone();
move || {
for i in 0..10 {
tx.send(i).unwrap();
}
}
});
let consumer = thread::spawn({
let data = Arc::clone(&data);
move || {
for value in rx {
let mut vec = data.lock().unwrap();
vec.push(value);
}
}
});
producer.join().unwrap();
consumer.join().unwrap();
println!("최종 데이터: {:?}", *data.lock().unwrap());
}
결론
- Arc는 다중 스레드에서 데이터를 안전하게 공유하도록 보장합니다.
- Mutex는 단일 스레드에서 데이터의 동기화를 보장하며,
Arc
와 함께 사용하면 다중 스레드 환경에서도 데이터를 안전하게 수정할 수 있습니다. - Rust의 Arc + Mutex 조합은 동시성 프로그래밍에서 안정성과 성능을 모두 제공하며, 특히 데이터 읽기-쓰기 작업이 필요한 경우 강력한 도구로 활용됩니다.
- 필요한 경우 채널, 반복자, 비동기 작업과 결합하여 더욱 복잡한 작업을 구현할 수 있습니다.