데드락이란?
정의
- 두 개 이상 스레드가 서로의 자원 해제를 기다리며 영원히 진행하지 못하는 상태입니다.
- CPU 사용률이 낮아도 서비스는 멈출 수 있어, "조용한 장애"로 나타나는 경우가 많습니다.
비슷한 문제와 구분
| 문제 | 특징 |
|---|
| Deadlock | 서로 기다리며 영구 정지 |
| Livelock | 계속 움직이지만 진전 없음 |
| Starvation | 특정 스레드만 계속 기아 상태 |
데드락의 4조건 (Coffman)
4조건
| 조건 | 의미 |
|---|
| 상호 배제 | 자원을 동시에 하나만 사용 가능 |
| 점유 대기 | 자원 하나를 가진 채 다른 자원 대기 |
| 비선점 | 강제로 자원 회수 불가 |
| 순환 대기 | A가 B를, B가 C를, C가 A를 기다림 |
핵심 원리
- 이 네 가지가 동시에 성립할 때 데드락이 가능합니다.
- 예방은 "4조건 중 최소 하나를 깨는 설계"라고 이해하면 쉽습니다.
대표 재현 시나리오
락 순서 역전
std::mutex m1, m2;
void T1() {
std::lock_guard<std::mutex> g1(m1);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard<std::mutex> g2(m2);
}
void T2() {
std::lock_guard<std::mutex> g1(m2);
std::this_thread::sleep_for(std::chrono::milliseconds(1));
std::lock_guard<std::mutex> g2(m1);
}
왜 멈추는가
- T1은
m1을 쥔 채 m2를 기다리고,
- T2는
m2를 쥔 채 m1을 기다립니다.
- 서로 풀어주길 기다리는 순환 대기가 완성됩니다.
예방 패턴 1 - 락 순서와 RAII
락 순서 규칙
- 팀/프로젝트 차원에서 전역 락 순서(계층) 를 정합니다. (예:
AccountLock -> InventoryLock -> WorldLock)
- 모든 코드 경로가 이 순서를 따르도록 강제하면 순환 대기를 크게 줄일 수 있습니다.
RAII로 누락 방지
- 수동
lock()/unlock() 대신 lock_guard, unique_lock을 기본으로 사용합니다.
- 예외/조기 반환 경로에서도 unlock 누락을 막을 수 있습니다.
예방 패턴 2 - 표준 라이브러리 활용
std::scoped_lock (권장)
std::mutex m1, m2;
void Safe() {
std::scoped_lock lock(m1, m2);
}
std::lock + adopt_lock
std::lock(m1, m2);
std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
std::lock_guard<std::mutex> g2(m2, std::adopt_lock);
- 복수 락 획득 로직을 직접 작성하지 말고 표준 도구를 우선 사용하세요.
예방 패턴 3 - try_lock/timeout과 복구
타임아웃 기반 방어
std::timed_mutex m;
bool TryWork() {
if (!m.try_lock_for(std::chrono::milliseconds(10))) {
return false;
}
std::lock_guard<std::timed_mutex> lock(m, std::adopt_lock);
return true;
}
실무 적용 포인트
- 온라인 서버는 "무한 대기"보다 "실패 후 복구" 전략이 운영상 안전한 경우가 많습니다.
- 단, 타임아웃은 데드락을 "숨길" 수 있으므로 원인 추적 로그를 반드시 남겨야 합니다.
탐지와 디버깅 체크리스트
관측 신호
- 요청 처리량이 갑자기 0에 가까워짐
- CPU는 낮은데 대기 스레드 수가 증가
- 특정 락 근처 스택 프레임이 여러 스레드에서 반복
점검 절차
- 스레드 덤프/콜스택 수집
- 각 스레드의 "보유 락"과 "대기 락"을 표로 정리
- 락 그래프에서 순환(Cycle) 존재 여부 확인
- 해당 경로를
scoped_lock/순서 통일로 수정
강의 시 유의사항
강조 포인트
- 데드락은 재현이 어려워 "코드 리뷰 단계 예방"이 가장 중요합니다.
- "락 순서 통일 + RAII + 표준 도구 사용"이 실무에서 가장 효과적입니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| unlock 누락만 고치면 데드락은 끝 | 락 순서 역전만으로도 데드락 발생 가능 |
try_lock을 쓰면 데드락이 사라진다 | 회피 확률을 높일 뿐 근본 원인 제거는 별개 |
| 데드락은 테스트에서 바로 잡힌다 | 타이밍 의존이라 라이브에서만 드러나기 쉬움 |
체크 질문 (스스로 답해보기)
- Coffman 4조건을 실제 코드 예시에 대응시켜 설명할 수 있는가?
- 두 mutex를 잡아야 할 때
scoped_lock이 왜 안전한가?
- timeout 기반 회피를 도입할 때 필수로 남겨야 하는 운영 로그는 무엇인가?