
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를 적극 활용하여, 모든 타입이 처리될 수 있도록 하자.