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;
}
정리
핵심 요약
- mutex: 상호 배제, 공유 데이터 보호
- lock_guard: 간단한 RAII 락
- unique_lock: 유연한 락 (수동 제어)
- scoped_lock: 여러 뮤텍스 (C++17)
- recursive_mutex: 재귀 잠금 가능
- 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 |