Deadlock
말 그대로 Lock이 죽었다라는 의미인데, thread들이 mutex lock이 해제되길 영원히 기다리는 상태를 뜻한다.
Example Code
std::mutex mtx;
void deadlockFn()
{
const std::lock_guard<std::mutex> lck(mtx);
const std::lock_guard<std::mutex> lck2(mtx);
}
int main()
{
std::thread t1(deadlockFn);
t1.join();
std::cout << "bye" << std::endl;
return 0;
}
mutex lock을 하나 잡아주고 이어서 mutex lock을 다시 잡아주게 된다면 영원히 기다리는 Deadlock 상태가 된다.
이를 그림으로 다시 보자면,
먼저, 첫 코드 라인에서 mutex lock을 획득한다. 크리티컬 섹션 안에서 다음 명령어를 실행하는데 이번에는 자신이 잠근 mutex를 다시 획득하려고 시도한다. 하지만 해당 mutex는 이미 잠겨있기 때문에 데드락 상태에 빠지게 된다. 위와 같은 경우는 셀프 데드락 상태이다.
실제 개발하면서 종종 발생될 예를 들면, 아래와 같이 mutex lock을 건 함수안에서 어떠한 함수를 호출하는데 그 안에 같은 mutex를 획득하려고 한다면 이는 셀프 데드락 상황이 된다.
Example Code
std::mutex mtx;
void fn()
{
const std::lock_guard<std::mutex> lck2(mtx);
}
void deadlockFn()
{
const std::lock_guard<std::mutex> lck(mtx);
fn();
}
int main()
{
std::thread t1(deadlockFn);
t1.join();
std::cout << "bye" << std::endl;
return 0;
}
이러한 코드는 올바르진 않지만 의도적으로 스스로에게 lock을 걸어야하는 경우가 있을 수 있다. 그럴 때는 std::recursive_mutex을 사용하면 아무런 문제 없이 셀프락을 걸 수 있다.
Example Code : recursive_mutex
std::recursive_mutex mtx;
void fn()
{
const std::lock_guard<std::recursive_mutex> lck2(mtx);
}
void deadlockFn()
{
const std::lock_guard<std::recursive_mutex> lck(mtx);
fn();
}
int main()
{
std::thread t1(deadlockFn);
t1.join();
std::cout << "bye" << std::endl;
return 0;
}
std::scoped_lock (C++17)
Multi thread 환경에서 말하는 데드락 상태와 scoped_lock에 대해 알아본다.
Example Code
std::mutex mtxA;
std::mutex mtxB;
void ab()
{
// A부터 획득 후 B 획득
const std::lock_guard<std::mutex> lckA(mtxA);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 데드락 상황을 만들기 위한 딜레이
const std::lock_guard<std::mutex> lckB(mtxB);
}
void ba()
{
// B부터 획득 후 A 획득
const std::lock_guard<std::mutex> lckB(mtxB);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 데드락 상황을 만들기 위한 딜레이
const std::lock_guard<std::mutex> lckA(mtxA);
}
int main()
{
std::thread t1(ab);
std::thread t2(ba);
t1.join();
t2.join();
std::cout << "bye" << std::endl;
return 0;
}
t1은 mutex A를 획득하고 B를 획득하기 위한 시도를 하고, t2는 mutex B를 획득하고 A를 획득하려고 시도하는 코드인데 이를 실행해보면 데드락 상태에 빠지는 것을 알 수 있다.
이를 그림으로 나타내 본다면,
각각의 thread는 mutex A, mutex B를 먼저 획득한다. 이어서 두번째 mutex를 획득하려고 하지만 모든 mutex가 이미 lock이 되어있기 때문에 t1은 mutex B를 획득하지 못하고 t2는 mutex A를 획득하지 못하는 상태가 된다. 이렇게 서로가 서로의 lock이 해제가 되길 영원히 기다리는 Deadlock 상태에 빠지게 되는 것이다.
이를 해결할 수 있는 방법은, mutex를 언제나 같은 순서로 걸어주는 것이다. 즉, 함수 ba()가 mutex A부터 걸어주고 mutex B를 걸어준다면 해당 코드는 데드락 상황 없이 정상적으로 종료된다. 하지만 문제는 2개 혹은 그 이상의 mutex를 사용할 때 어떠한 mutex에 먼저 락을 먼저 걸어주는지 순서를 기억하면서 코딩하기는 어렵다.
이를 해결하기 위한 방법은 std::scoped_lock을 사용하는 것이다. scoped_lock 역시 RAII idom을 따르며 여러 mutex가 주어졌을 때 데드락을 피할 수 있는 알고리즘이 탑재되어 있다.
Example Code : scoped_lock 사용
std::mutex mtxA;
std::mutex mtxB;
void ab()
{
// A부터 획득 후 B 획득
const std::scoped_lock lck(mtxA, mtxB);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 데드락 상황을 만들기 위한 딜레이
}
void ba()
{
// B부터 획득 후 A 획득
const std::scoped_lock lck(mtxB, mtxA);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 데드락 상황을 만들기 위한 딜레이
}
int main()
{
std::thread t1(ab);
std::thread t2(ba);
t1.join();
t2.join();
std::cout << "bye" << std::endl;
return 0;
}
이렇게 std::scoped_lock을 사용한다면 mutex lock을 거는 순서와 상관없이 알아서 내부적으로 정한 mutex lock 순서로 lock 획득을 시도하여 정상적으로 종료가 되는 것을 확인할 수 있다. std::scoped_lock은 c++17부터 도입이 되었기 때문에 그 이전 버전에서는 std::lock_guard와 std::lock을 혼합해서 사용해야하는데 훨씬 더 복잡하고 버그가 들어가기 쉬운 코드가 만들어진다. 그렇기 때문에 최소 C++17을 사용하여 멀티 스레드 프로그래밍을 하는 것이 좋다.
하나의 mutex lock을 하고 싶으면 lock_guard를 사용하면 되고,
여러개의 mutex lock을 사용하고 싶은 경우 scoped_lock을 사용하면 된다.
'C++ > Thread' 카테고리의 다른 글
[Thread] std::call_once (0) | 2022.07.18 |
---|---|
[Thread] std::shared_mutex (0) | 2022.07.17 |
[Thread] std::unique_lock (0) | 2022.07.17 |
[Thread] std::mutex (0) | 2022.07.15 |
[Thread] Mutex(뮤텍스) (0) | 2022.07.15 |