[2026] C++ Mutex & Lock | 뮤텍스와 락 가이드
이 글의 핵심
C++ Mutex & Lock: 뮤텍스와 락 가이드. mutex 기본·lock_guard (RAII).
들어가며
Mutex(뮤텍스)는 상호 배제(Mutual Exclusion)를 위한 동기화 도구입니다. 여러 스레드가 공유 데이터에 동시 접근하는 것을 방지하여 데이터 레이스를 막습니다.
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
1. std::mutex 기본
기본 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
문제: 예외 안전성
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
자동 잠금/해제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)도 줄일 수 있습니다.
예외 안전
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
유연한 잠금
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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;
}
지연 잠금
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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. 여러 뮤텍스
데드락 문제
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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 사용
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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)
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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. 실전 예제: 스레드
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. 안전 카운터 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#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