뮤텍스와 락

Jaemyeong Lee·2024년 12월 26일

게임 서버1

목록 보기
114/220

왜 뮤텍스가 필요한가

임계 영역(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);
}  // 스코프 종료 시 자동 unlock
  • 가장 단순하고 안전한 기본 선택지입니다.
  • 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);  // 복수 mutex를 데드락 회피 방식으로 획득
    // 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

항목atomicmutex
강점단일 상태 갱신 빠름복합 상태/불변식 보호 강함
약점복합 규칙 표현 어려움경합 시 대기 비용
대표 예카운터, 플래그컨테이너 수정, 트랜잭션성 갱신
  • 단일 카운터면 atomic이 적합할 수 있습니다.
  • 여러 필드를 함께 일관되게 바꿔야 하면 mutex가 정석입니다.

강의 시 유의사항

강조 포인트

  • 락의 목적은 성능보다 먼저 정확성입니다.
  • 기본값은 lock_guard, 필요한 경우에만 unique_lock을 선택하세요.
  • 복수 락은 scoped_lock을 습관화해 데드락 위험을 낮추세요.

자주 하는 오해

오해바로잡기
락이 많을수록 안전하다안전은 늘지만 경합으로 처리량이 크게 떨어질 수 있음
unique_lock이 항상 더 좋다단순 구간은 lock_guard가 더 간결하고 실수 적음
reserve()하면 컨테이너 동시 수정도 안전메모리 재할당과 동시성 안전은 다른 문제

체크 질문 (스스로 답해보기)

  • 임계 영역 안에서 금지해야 할 작업은 무엇인가?
  • 두 mutex를 잡아야 할 때 락 순서 문제를 어떻게 예방할 것인가?
  • 어떤 상황에서 atomic 대신 mutex가 더 적합한가?

profile
李家네_공부방

0개의 댓글