[Server] #15-8. 데드락
데드락
Deadlock(교착상태)이란 여러 개의 스레드가 서로 상대방이 점유한 락을 기다리면서 무한 대기 상태에 빠진 현상을 말한다. 즉, 두 개 이상의 스레드가 서로 상대방이 가진 자원을 해제할 때까지 기다리기 때문에 프로그램이 멈춰버리는 문제이다.
발생 원인
데드락이 발생하려면 4가지 조건이 모두 성립해야 한다.
- 상호 배제(Mutual Exclusion)
- 한 번에 한 스레드만 자원을 사용할 수 있어야 한다.
- 점유 대기(Hold and Wait)
- 스레드가 이미 하나의 자원을 점유한 상태에서 추가로 다른 자원을 기다리는 상황이다.
- 비선점(No Preemption)
- 다른 스레드가 점유한 자원을 강제로 빼앗을 수 없어야 한다.
- 순환 대기(Circular Wait)
- 여러 스레드가 사이클 구조로 자원을 점유한 상태에서 서로의 자원을 기다리고 있어야 한다. - 이 네 가지 조건이 모두 충족될 때 데드락이 발생할 수 있다.
데드락 실습
다음은 데드락이 발생하는 예제이다.
std::mutex m1; // Account Manager
std::mutex m2; // User Manager
void Thread_1()
{
for (int i = 0; i < 10000; i++)
{
std::lock_guard<std::mutex> lockGuard1(m1);
std::lock_guard<std::mutex> lockGuard2(m2);
}
}
void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
std::lock_guard<std::mutex> lockGuard1(m2);
std::lock_guard<std::mutex> lockGuard2(m1);
}
}
int main()
{
std::thread t1(Thread_1);
std::thread t2(Thread_2);
t1.join();
t2.join();
std::cout << "Jobs Done" << std::endl;
}
- 분석
- Thread_1은
m1
을 먼저 잡고m2
를 잡으려고 한다. - Thread_2는
m2
를 먼저 잡고m1
을 잡으려고 한다. - Thread_1은 Thread_2가
m2
를 해제할 때까지 기다리고, Thread_2는 Thread_1이m1
을 해제할 때까지 기다린다. - 두 스레드가 서로의 락을 기다리면서 무한 대기 상태(데드락)에 빠진다.
- Thread_1은
데드락 해결 방법
- 락 획득 순서 통일하기
- 락을 항상 일정한 순서로 획득하면 데드락을 예방할 수 있다.
- Thread_2의 락 획득 순서를 Thread_1과 동일하게 변경하면, 순환 대기 조건이 깨져 데드락이 발생하지 않는다.
void Thread_2()
{
for (int i = 0; i < 10000; i++)
{
std::lock_guard<std::mutex> lockGuard1(m1);
std::lock_guard<std::mutex> lockGuard2(m2);
}
}
추가 설명
- 디버깅
- 위의 예제는 스레드가 2개라서 단순한 경우지만, 멀티 스레드 환경에서는 락을 잡는 순서가 복잡해지면서 문제 해결이 어려워진다.
- 버그를 찾는 건 쉬울 수 있지만, 시스템 구조가 복잡할수록 전체적인 영향을 고려해야 하기 때문에 고치는 건 어려울 수 있다.
- 개발 vs 라이브 환경
- 개발 단계에서는 문제 없이 실행 됐는데, 라이브 환경에서는 데드락이 발생할 수 있다. → 접속자가 늘어날 경우. 즉, 테스트 환경에서는 쉽게 재현되지 않을 수 있다.
- 예제에서도 for문의 반복 횟수를 줄이면 데드락이 발생하지 않을 수 있다.
- 데드락 탐지법
- 락 구조를 그래프로 표현하고, 사이클이 존재하는지 확인한다.
- 결론
- 데드락의 근본적인 원인은 락을 잡는 순서가 꼬이는 것이다. 락을 일정한 순서로 잡으면 데드락을 예방할 수 있다.
- 하지만 완벽하게 처리하는 방법은 존재하지 않는다. 규모가 작은 프로젝트에서는 데드락이 거의 발생하지 않지만, 대규모 시스템에서는 데드락 탐지 및 우회 기법을 사용해 우회하기도 한다.
Leave a comment