데드락은 여러 스레드 또는 프로세스가 서로 다른 스레드 또는 프로세스가 보유하고 있는 자원을 기다리면서 모두 실행을 멈추는 상황을 말한다.
데드락의 발생 조건은 크게 4가지로 구분되는데, 이를 Coffman 조건이라고 한다. 이 조건들은 다음과 같다.
상호 배제(Mutual Exclusion) : 자원은 한 번에 한 프로세스만이 사용할 수 있다.
점유 대기(Hold and Wait) : 프로세스가 자원을 가진 상태에서 다른 프로세스가 사용하고 있는 자원을 기다린다.
비선점(No Preemption) : 한 프로세스가 다른 프로세스의 자원을 강제로 빼앗을 수 없다.
환형 대기(Circular Wait) : 서로를 기다리는 프로세스들이 원형으로 대기하게 된다.
std::mutex mutex1, mutex2;
void ThreadA()
{
// ThreadA가 mutex1을 먼저 얻습니다.
mutex1.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
// ThreadA는 mutex2를 얻기 위해 기다립니다.
mutex2.lock();
std::cout << "ThreadA has both mutexes\n";
mutex2.unlock();
mutex1.unlock();
}
void ThreadB()
{
// ThreadB는 mutex2를 먼저 얻습니다.
mutex2.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
// ThreadB는 mutex1을 얻기 위해 기다립니다.
mutex1.lock();
std::cout << "ThreadB has both mutexes\n";
mutex1.unlock();
mutex2.unlock();
}
int main()
{
std::thread t1(ThreadA);
std::thread t2(ThreadB);
t1.join();
t2.join();
return 0;
}
위의 예제 코드에서는 ThreadA와 ThreadB 두 스레드가 각각 mutex1과 mutex2를 잠그고, 서로의 뮤텍스를 얻으려고 기다리는 상황이 발생하므로 데드락이 발생한다.
앞선 예제에서 봤듯이, 스레드들이 뮤텍스를 얻는 순서가 일관성이 없으면 데드락이 발생할 가능성이 크다. 따라서, 모든 스레드가 뮤텍스를 얻는 순서를 일관성 있게 유지하는 것이 중요하다.
예를 들어, 아래와 같이 순서를 변경해서 뮤텍스를 얻는다면, 데드락을 피할 수 있다.
코드를 std::mutex mutex1, mutex2;
void ThreadA()
{
// ThreadA가 mutex1을 먼저 얻습니다.
mutex1.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
// ThreadA는 mutex2를 얻습니다.
mutex2.lock();
std::cout << "ThreadA has both mutexes\n";
mutex2.unlock();
mutex1.unlock();
}
void ThreadB()
{
// ThreadB도 mutex1을 먼저 얻습니다.
mutex1.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
// 그 다음 mutex2를 얻습니다.
mutex2.lock();
std::cout << "ThreadB has both mutexes\n";
mutex2.unlock();
mutex1.unlock();
}
int main()
{
std::thread t1(ThreadA);
std::thread t2(ThreadB);
t1.join();
t2.join();
return 0;
}
C++에서는 std::lock()이라는 함수를 제공하여 여러 뮤텍스를 한 번에 잠글 수 있다. 이 함수는 주어진 모든 뮤텍스가 잠길 때까지 기다린다.
std::lock() 함수는 뮤텍스를 순서대로 잠그는 것이 아니라, 한 번에 모두 잠그므로 데드락이 발생하는 것을 방지할 수 있다.
std::mutex mutex1, mutex2;
void ThreadA()
{
// 두 뮤텍스를 한 번에 잠금
std::lock(mutex1, mutex2);
// adopt_lock : 이미 lock된 상태니까, 나중에 소멸될 때 풀어준다.
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
std::cout << "ThreadA has both mutexes\n";
}
void ThreadB()
{
// 두 뮤텍스를 한 번에 잠금
std::lock(mutex1, mutex2);
// adopt_lock : 이미 lock된 상태니까, 나중에 소멸될 때 풀어준다.
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
std::cout << "ThreadB has both mutexes\n";
}
int main()
{
std::thread t1(ThreadA);
std::thread t2(ThreadB);
t1.join();
t2.join();
return 0;
}
이처럼, std::lock() 함수와 std::lock_guard를 사용하면 여러 뮤텍스를 안전하게 잠글 수 있다. std::lock() 함수는 뮤텍스를 한 번에 잠그며, std::lock_guard는 뮤텍스의 소유권을 자동으로 관리하여 코드의 안정성을 높여준다.