Mutex
는 공유자원 관리를 위한 API를 제공하는 구조체이다. Rust뿐만 아니라 많은 언어들에서, Mutex
에 의해 wrapping되어 있는 데이터에 대한 접근은, 이미 Lock
을 획득한 다른 Thread가 있는 경우 접근하는 Thread에 대해 os레벨의 sleep을 거는 방식으로 CPU clock에 대한 손실을 최소화한다. 요약하자면
Lock
이 획득된 적이 없는 경우 -> 바로 데이터 접근Lock
이 획득 되었고, 아직 Unlock
되지 않은 경우 -> Os sleep아래 코드는 Mutex 사용 예시이다.
// create mutex wrapped in Arc
let mutex = Arc::new(Mutex::new(0));
// Put it to background thread
let _ = std::thread::spawn({
let mutex = mutex.clone();
move || {
let mut value = mutex.lock().unwrap();
// get update value
*value = 1;
}
});
sleep(Duration::from_secs(1));
// get updated value
let updated_value = mutex.lock().unwrap();
println!("{}", *updated_value);
Mutex poisoning
은 mutexed value
의 일관성을 보장하기 위한 매커니즘이다. 한 쓰레드에서 mutex value
를 얻은 후 panic
이 일어난 경우, 해당 Mutex에 접근 시, PoisonError
를 반환하게 된다.
let mutex = Arc::new(Mutex::new(0));
let _ = std::thread::spawn({
let mutex = mutex.clone();
move || {
let mut value = mutex.lock().unwrap();
*value = 1;
panic!("Panic Point")
}
});
sleep(Duration::from_secs(1));
if let Err(_) = mutex.lock() {
println!("poisoned!");
};
값의 일관성은 대부분의 경우 굉장히 중요한 문제이지만, 특정 thread에서 panic이 발생하면 어떤 일이 일어났던지 poison 마킹을 하는 이러한 방식은 아래의 케이스에서 문제를 발생시킬수 있다.
Mutexed value
를 변경하지 않았지만 Guard만 얻어 놓은 경우
let _ = std::thread::spawn({
let mutex = mutex.clone();
move || {
let mut value = mutex.lock().unwrap();
//*value = 1;
panic!("Panic Point")
}
});
위 처럼, Mutex
를 얻었지만 값을 변경하지 않고 read만 thread에서, 그리고 그 과정에서 panic이 발생한 경우, 다른 thread에서 해당 값에 접근시 PoisonError
를 일으키면, 해당 값에 접근 하는 모든 thread에서 unwrap()
사용시 패닉을 일으키게 되고, 결국 전체 시스템 장애로 이어지게 될 것이다.
가령 Database connection과 같은 경우가 이에 해당한다.
이러한 경우의 해결법은 아래와 같다.
let _ = std::thread::spawn({
let mutex = mutex.clone();
move || {
let mut value = mutex.lock().unwrap();
panic!("Panic Point")
}
});
let value = mutex.lock().unwrap_or_else(PoisonError::into_inner);
println!("value: {}", value);
하지만 언제나 그렇듯이, 가장 좋은 해결법은 문제가 발생되지 않도록 하는 것이다. 예방적 차원에서 위의 접근방법은 고려될 수 있으나, 근원적으로 Panic이 아닌, 사용자의 정의에 따라 Result
를 적극 활용하여, 모든 타입이 처리될 수 있도록 하자.