C++ Mutex & Lock | "뮤텍스와 락" 가이드

C++ Mutex & Lock | "뮤텍스와 락" 가이드

이 글의 핵심

C++ Mutex & Lock에 대한 실전 가이드입니다.

들어가며

Mutex(뮤텍스)상호 배제(Mutual Exclusion)를 위한 동기화 도구입니다. 여러 스레드가 공유 데이터에 동시 접근하는 것을 방지하여 데이터 레이스를 막습니다.


1. std::mutex 기본

기본 사용

#include <mutex>
#include <thread>
#include <iostream>

// std::mutex: 상호 배제(Mutual Exclusion)를 위한 뮤텍스
// 여러 스레드가 동시에 접근하지 못하도록 보호
std::mutex mtx;
int sharedData = 0;  // 공유 데이터 (여러 스레드가 접근)

void increment() {
    // mtx.lock(): 뮤텍스 잠금 (다른 스레드는 대기)
    // 이미 잠긴 경우 unlock될 때까지 블로킹
    mtx.lock();
    
    // 임계 영역 (Critical Section): 한 번에 한 스레드만 실행
    ++sharedData;
    
    // mtx.unlock(): 뮤텍스 해제 (다른 스레드가 진입 가능)
    mtx.unlock();
}

int main() {
    // 두 스레드가 동시에 increment 실행
    std::thread t1(increment);
    std::thread t2(increment);
    
    // join(): 스레드 종료 대기
    t1.join();
    t2.join();
    
    // 뮤텍스 덕분에 데이터 레이스 없이 안전하게 증가
    std::cout << "sharedData: " << sharedData << std::endl;  // 2
    
    return 0;
}

문제: 예외 안전성

#include <mutex>
#include <stdexcept>
#include <iostream>

std::mutex mtx;

void unsafeFunction() {
    mtx.lock();
    
    // 예외 발생 시 unlock 안됨!
    if (someCondition) {
        throw std::runtime_error("에러");
    }
    
    mtx.unlock();  // 실행 안됨
}

int main() {
    try {
        unsafeFunction();
    } catch (...) {
        std::cout << "예외 발생, 뮤텍스 잠김!" << std::endl;
    }
    
    return 0;
}

문제의 본질: unsafeFunction 중간에 예외가 나면 unlock 줄에 도달하지 못해 뮤텍스가 영구히 잠긴 상태가 될 수 있습니다. 이후 같은 뮤텍스를 기다리는 스레드는 전부 교착에 가까운 행렬로 멈춥니다.


2. lock_guard (RAII)

자동 잠금/해제

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
int sharedData = 0;

void safeIncrement() {
    std::lock_guard<std::mutex> lock(mtx);
    ++sharedData;
    // 소멸자에서 자동 unlock
}

int main() {
    std::thread t1(safeIncrement);
    std::thread t2(safeIncrement);
    
    t1.join();
    t2.join();
    
    std::cout << "sharedData: " << sharedData << std::endl;  // 2
    
    return 0;
}

패턴 설명: lock_guard는 생성자에서 잠그고 소멸자에서 풀기 때문에, 함수 중간에 return이나 예외가 나도 해제가 보장됩니다. 가드 객체의 수명을 최소 임계구역으로만 제한하면 경합(contention)도 줄일 수 있습니다.

예외 안전

#include <mutex>
#include <stdexcept>
#include <iostream>

std::mutex mtx;

void safeFunction() {
    std::lock_guard<std::mutex> lock(mtx);
    
    // 예외 발생해도 자동 unlock
    if (someCondition) {
        throw std::runtime_error("에러");
    }
    
    // lock 소멸자에서 unlock
}

int main() {
    try {
        safeFunction();
    } catch (...) {
        std::cout << "예외 발생, 뮤텍스 자동 해제" << std::endl;
    }
    
    return 0;
}

3. unique_lock

유연한 잠금

#include <mutex>
#include <iostream>

std::mutex mtx;

void flexibleLock() {
    std::unique_lock<std::mutex> lock(mtx);
    
    // 작업 1
    std::cout << "작업 1" << std::endl;
    
    // 수동 해제
    lock.unlock();
    
    // 락 없이 작업
    std::cout << "락 없는 작업" << std::endl;
    
    // 다시 잠금
    lock.lock();
    
    // 작업 2
    std::cout << "작업 2" << std::endl;
    
    // 소멸자에서 자동 unlock
}

int main() {
    flexibleLock();
    return 0;
}

지연 잠금

#include <mutex>
#include <iostream>

std::mutex mtx;

void deferredLock() {
    // 생성 시 잠금 안함
    std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
    
    // 조건부 잠금
    if (needLock) {
        lock.lock();
    }
    
    // 작업 수행
}

int main() {
    deferredLock();
    return 0;
}

4. 여러 뮤텍스

데드락 문제

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;

// ❌ 데드락 가능
void badTransfer() {
    // Thread 1: mtx1 → mtx2
    mtx1.lock();
    mtx2.lock();
    
    balance1 -= 50;
    balance2 += 50;
    
    mtx2.unlock();
    mtx1.unlock();
}

void anotherBadTransfer() {
    // Thread 2: mtx2 → mtx1 (데드락!)
    mtx2.lock();
    mtx1.lock();
    
    balance2 -= 30;
    balance1 += 30;
    
    mtx1.unlock();
    mtx2.unlock();
}

std::lock 사용

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;

// ✅ std::lock (데드락 방지)
void safeTransfer() {
    std::lock(mtx1, mtx2);  // 원자적으로 둘 다 잠금
    
    std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
    std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
    
    balance1 -= 50;
    balance2 += 50;
    
    std::cout << "이체 완료: " << balance1 << ", " << balance2 << std::endl;
}

int main() {
    std::thread t1(safeTransfer);
    std::thread t2(safeTransfer);
    
    t1.join();
    t2.join();
    
    return 0;
}

scoped_lock (C++17)

#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx1, mtx2;

// ✅ C++17: scoped_lock (더 간결)
void modernTransfer() {
    std::scoped_lock lock(mtx1, mtx2);
    
    // 작업 수행
    std::cout << "이체 수행" << std::endl;
    
    // 소멸자에서 자동 unlock
}

int main() {
    std::thread t1(modernTransfer);
    std::thread t2(modernTransfer);
    
    t1.join();
    t2.join();
    
    return 0;
}

5. 뮤텍스 종류

recursive_mutex

#include <mutex>
#include <iostream>

std::recursive_mutex rmtx;

void func1() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "func1" << std::endl;
}

void func2() {
    std::lock_guard<std::recursive_mutex> lock(rmtx);
    std::cout << "func2" << std::endl;
    func1();  // OK: 재귀 뮤텍스
}

int main() {
    func2();
    return 0;
}

timed_mutex

#include <mutex>
#include <thread>
#include <iostream>

std::timed_mutex tmtx;

void tryLockFor() {
    // 100ms 동안 시도
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "락 획득" << std::endl;
        
        // 작업 수행
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
        
        tmtx.unlock();
    } else {
        std::cout << "락 획득 실패 (타임아웃)" << std::endl;
    }
}

int main() {
    std::thread t1(tryLockFor);
    std::thread t2(tryLockFor);
    
    t1.join();
    t2.join();
    
    return 0;
}

6. 실전 예제: 스레드 안전 카운터

#include <mutex>
#include <thread>
#include <vector>
#include <iostream>

class ThreadSafeCounter {
    mutable std::mutex mtx;
    int count = 0;
    
public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        ++count;
    }
    
    void decrement() {
        std::lock_guard<std::mutex> lock(mtx);
        --count;
    }
    
    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return count;
    }
    
    void reset() {
        std::lock_guard<std::mutex> lock(mtx);
        count = 0;
    }
};

int main() {
    ThreadSafeCounter counter;
    std::vector<std::thread> threads;
    
    // 10개 스레드가 각각 1000번 증가
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&counter]() {
            for (int j = 0; j < 1000; ++j) {
                counter.increment();
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    std::cout << "최종 카운트: " << counter.get() << std::endl;  // 10000
    
    return 0;
}

정리

핵심 요약

  1. mutex: 상호 배제, 공유 데이터 보호
  2. lock_guard: 간단한 RAII 락
  3. unique_lock: 유연한 락 (수동 제어)
  4. scoped_lock: 여러 뮤텍스 (C++17)
  5. recursive_mutex: 재귀 잠금 가능
  6. timed_mutex: 시간 제한 잠금

Lock 비교

Lock특징오버헤드사용 시기
lock_guard간단, RAII낮음기본
unique_lock유연, 수동 제어중간조건부 잠금
scoped_lock여러 뮤텍스낮음다중 뮤텍스 (C++17)

실전 팁

사용 원칙:

  • 기본은 lock_guard
  • 수동 제어 필요 시 unique_lock
  • 여러 뮤텍스는 scoped_lock (C++17)
  • 재귀 잠금은 recursive_mutex

데드락 방지:

  • 뮤텍스 잠금 순서 통일
  • std::lock으로 원자적 잠금
  • scoped_lock 사용 (C++17)
  • 잠금 시간 최소화

성능:

  • 임계 영역 최소화
  • lock_guard가 가장 빠름
  • unique_lock은 필요 시만
  • 읽기 전용은 shared_mutex (C++17)

다음 단계

  • C++ Atomic
  • C++ Condition Variable
  • C++ Thread

관련 글

  • C++ async & launch |
  • C++ Atomic Operations |