Mutex와 Lock Guard

김민서·2025년 11월 20일

C/C++

목록 보기
4/6

Mutex(Mutual Exclusion)

Mutex는 멀티스레드 환경에서 공유 자원에 대한 접근을 제어하는 동기화 객체다.
여러 개의 스레드가 동시에 같은 자원에 접근할 때 발생할 수 있는 Race Condition을 방지하기 위해 사용된다.

코드 예시

#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        counter++; // Critical Section
        mtx.unlock();
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << counter << std::endl;
    return 0;
}

주요 메서드

  • lock()
    • 블로킹 방식으로 mutex를 잠근다.
    • 이미 다른 스레드가 잠금을 획득한 상태라면 해제될 때까지 대기한다.
  • unlock()
    • 획득한 잠금을 해제한다.
    • 잠금을 획득하지 않은 스레드에서 호출하는 것은 정의되지 않은 동작이다.
  • try_lock()
    • 논블로킹 방식으로 잠금을 시도하며, 성공 시 true, 실패 시 즉시 false를 반환한다.
    • 잠금 획득을 기다리지 않고 다른 작업을 수행할 수 있다.

위 예시 코드처럼 lock()unlock() 사이의 코드 영역(Critical Section)는 한 번에 하나의 스레드만 실행할 수 있다.
하지만 unlock() 호출 전에 예외가 발생하거나 조기 반환하면 데드락이 발생한다.

특수한 상황을 위한 Mutex

  • recursive_mutex
    • 같은 스레드가 여러 번 잠금을 획득할 수 있는 mutex다.
    • 잠금 횟수만큼 unlock()을 호출해야 완전히 해제된다.
  • timed_mutex
    • 타임아웃을 지정할 수 있는 mutex다.
    • try_lock_for()try_lock_until() 메서드를 제공한다.
std::timed_mutex tmtx;

void function() {
    if (tmtx.try_lock_for(std::chrono::seconds(1))) {
        // 1초 이내에 잠금 획득 성공
        // Critical Section
        tmtx.unlock();
    } else {
        // 타임아웃 발생
    }
}
  • shared_mutex
    • Reader-Writer Lock을 구현한 mutex다.
    • C++17부터 지원한다.
    • 여러 스레드가 동시에 읽기 작업을 수행할 수 있지만, 쓰기 작업은 배타적으로 수행된다.

Lock Guard

수동으로 lock()과 unlock()을 관리하는 것은 위험하다. 이 문제를 해결하는 방법이 RAII(Resource Acquisition Is Initialization) 패턴이다. 객체의 생성자에서 자원을 획득하고, 소멸자에서 자원을 해제하는 방식으로, C++의 스택 해제(stack unwinding) 메커니즘을 활용하여 예외가 발생하더라도 자원이 안전하게 해제되도록 보장한다.
lock guard는 C++ 표준 라이브러리에서 제공하는 클래스중 하나로 mutex 관리에 대한 실수를 줄일 수 있도록 해준다.
lock guard를 사용하면 mutex를 자동으로 잠그고 해당 범위가 벗어나면 lock_guard의 소멸자가 호출되어 자동으로 mutex를 해제한다.

lock_guard

  • 가장 기본적인 RAII 기반 mutex 래퍼다.
  • 생성 시 자동으로 lock()을 호출하고, 소멸 시 자동으로 unlock()을 호출한다.
  • 가장 단순하고 오버헤드가 적다.
  • 생성 즉시 잠금을 획득한다.
  • 잠금 해제 시점을 제어할 수 없다.
  • 복사나 이동이 불가능하다.
#include <mutex>
#include <thread>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 생성 시 lock
        counter++;
        // 스코프를 벗어나면 자동으로 unlock
    }
}

void safe_function() {
    std::lock_guard<std::mutex> lock(mtx);
    if (some_condition)
        return;

    process_data();  // 예외 발생해도 unlock 보장
}

unique_lock

  • 지연 잠금, 조건 변수 연동, 소유권 이전 등 다양한 기능을 제공한다.
  • 수동으로 lock()/unlock() 호출 가능하다.
  • condition_variable과 같은 조건 변수와 함께 사용이 필수적이다.
  • 이동은 가능하지만 복사 불가능하다.
std::mutex mtx;

void flexible_function() {
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);  // 지연 잠금

    prepare_data();

    lock.lock();  // 필요한 시점에 수동으로 잠금
    modify_shared_data();
    lock.unlock();  // 수동으로 해제 가능

    cleanup(); // 잠금 없이 다른 작업
    
    // 스코프 종료 시 잠금 상태면 자동으로 unlock
}
// 조건 변수와의 사용 (가장 흔한 케이스)
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void wait_for_signal() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; });  // unique_lock 필수
    process_data();
}

scoped_lock

  • C++17부터 지원한다.
  • 여러 개의 mutex를 동시에 잠그는 경우 데드락을 방지하기 위해 사용한다.
  • 내부적으로 std::lock() 알고리즘을 사용한다.
  • 단일 mutex에 대해서는 lock_guard와 동일하다.
std::mutex mtx1, mtx2;

// 데드락 발생 가능한 코드
void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::lock_guard<std::mutex> lock2(mtx2);  // 순서 문제
    // ...
}

void thread2() {
    std::lock_guard<std::mutex> lock2(mtx2);
    std::lock_guard<std::mutex> lock1(mtx1);  // 반대 순서!
    // ...
}

// scoped_lock으로 데드락 방지
void safe_thread1() {
    std::scoped_lock lock(mtx1, mtx2);  // 데드락 회피 알고리즘 사용
    // ...
}

void safe_thread2() {
    std::scoped_lock lock(mtx2, mtx1);  // 순서 상관없이 안전
    // ...
}

Lock Guard는 RAII 패턴을 활용하여 mutex를 안전하게 관리하는 방법이다.
예외 발생이나 조기 반환 상황에서도 자동으로 잠금이 해제되므로, 수동으로 lock()/unlock()을 관리하는 것보다 훨씬 안전하다.
대부분의 경우 lock_guard로 충분하며, 특수한 상황에서만 unique_lock이나 scoped_lock을 사용하면 된다.
이들을 적절히 활용하면 멀티스레드 프로그래밍에서 발생할 수 있는 많은 버그를 예방할 수 있다.

profile
시스템 개발 공부 중

0개의 댓글