왜 뮤텍스가 필요한가
임계 영역(Critical Section)
- 여러 스레드가 동시에 접근하면 안 되는 코드/데이터 구간을 임계 영역이라고 합니다.
- 대표 예: 공용
vector 수정, 계정 잔액 갱신, 월드 상태 맵 업데이트
- 임계 영역은 한 번에 한 스레드만 들어가야 데이터 불변식이 유지됩니다.
상호 배타(Mutual Exclusion)
- 뮤텍스는 "문" 역할을 합니다.
- 들어갈 때 lock, 나올 때 unlock으로 동시 진입을 막습니다.
- 핵심 목표는 "빠르게 만들기"보다 "정확하게 만들기"입니다.
std::mutex 기본 사용
수동 lock/unlock
std::mutex m;
std::vector<int> v;
void Push(int x) {
m.lock();
v.push_back(x);
m.unlock();
}
수동 방식의 위험
return, 예외, 조기 종료 경로에서 unlock() 누락 위험이 큽니다.
- 누락되면 다른 스레드는 영원히 대기할 수 있습니다(데드락 유사 증상).
- 실무에서는 수동 lock/unlock을 최소화하고 RAII 락을 기본으로 사용합니다.
RAII 락 도구
std::lock_guard (기본값)
std::mutex m;
std::vector<int> v;
void Push(int x) {
std::lock_guard<std::mutex> lock(m);
v.push_back(x);
}
- 가장 단순하고 안전한 기본 선택지입니다.
- lock/unlock 수동 호출을 없애 실수를 줄입니다.
std::unique_lock (유연성 필요 시)
std::mutex m;
void Work() {
std::unique_lock<std::mutex> lock(m, std::defer_lock);
lock.lock();
lock.unlock();
}
- 지연 잠금(
defer_lock), 수동 unlock/relock, 소유권 이동 등 고급 제어가 가능합니다.
condition_variable::wait와 함께 사용할 때 필수입니다.
std::scoped_lock (복수 락)
std::mutex a, b;
void Transfer() {
std::scoped_lock lock(a, b);
}
- 두 개 이상 락을 동시에 잡아야 할 때 권장됩니다.
- 수동 순서 관리보다 안전하며 코드 의도가 명확합니다.
락 경합과 락 범위 설계
"락 걸면 끝"이 아닌 이유
- 락은 정확성을 보장하지만, 과도하면 경합(contention)으로 성능이 급격히 떨어집니다.
- 임계 영역 안에서 오래 걸리는 작업(I/O, DB, 로그 포맷팅)을 하면 전체 처리량이 감소합니다.
락 범위(Granularity) 비교
| 방식 | 장점 | 단점 | 추천 상황 |
|---|
| 굵은 락(Coarse) | 구현 단순, 버그 적음 | 병렬성 낮음 | 초기 구현/안정화 단계 |
| 가는 락(Fine) | 병렬성 높음 | 복잡도/데드락 위험 증가 | 병목 확인 후 점진 적용 |
실무 규칙
- 먼저 굵은 락으로 정확성을 확보
- 프로파일링으로 병목 확인 후 좁혀 나가기
- "락 개수 증가"보다 "공유 데이터 축소"가 우선입니다
데드락 회피 기본 규칙
락 순서 통일
- 모든 코드 경로에서 동일한 순서로 락을 획득합니다. (예: 항상
A -> B)
- 팀 규칙(락 계층)을 문서화하면 재발을 크게 줄일 수 있습니다.
임계 영역 안에서 금지할 것
- 블로킹 I/O, 외부 API 호출, 오래 걸리는 연산
- 콜백/가상함수 호출(잠금 재진입·예상 못한 락 획득 유발)
복수 락은 도구 사용
- C++17 이상이면
std::scoped_lock
- 또는
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);
자주 하는 질문: mutex vs atomic
| 항목 | atomic | mutex |
|---|
| 강점 | 단일 상태 갱신 빠름 | 복합 상태/불변식 보호 강함 |
| 약점 | 복합 규칙 표현 어려움 | 경합 시 대기 비용 |
| 대표 예 | 카운터, 플래그 | 컨테이너 수정, 트랜잭션성 갱신 |
- 단일 카운터면
atomic이 적합할 수 있습니다.
- 여러 필드를 함께 일관되게 바꿔야 하면
mutex가 정석입니다.
강의 시 유의사항
강조 포인트
- 락의 목적은 성능보다 먼저 정확성입니다.
- 기본값은
lock_guard, 필요한 경우에만 unique_lock을 선택하세요.
- 복수 락은
scoped_lock을 습관화해 데드락 위험을 낮추세요.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| 락이 많을수록 안전하다 | 안전은 늘지만 경합으로 처리량이 크게 떨어질 수 있음 |
unique_lock이 항상 더 좋다 | 단순 구간은 lock_guard가 더 간결하고 실수 적음 |
reserve()하면 컨테이너 동시 수정도 안전 | 메모리 재할당과 동시성 안전은 다른 문제 |
체크 질문 (스스로 답해보기)
- 임계 영역 안에서 금지해야 할 작업은 무엇인가?
- 두 mutex를 잡아야 할 때 락 순서 문제를 어떻게 예방할 것인가?
- 어떤 상황에서
atomic 대신 mutex가 더 적합한가?