std::mutex, lock_guard, unique_lock (Step-by-Step)이 문서는 제공된 강의 텍스트를 바탕으로 핵심 개념을 흐트러뜨리지 않도록 정리한 완전 학습 파일입니다.
실습용 코드, 단계별 체크리스트, 퀴즈, 요약까지 한 번에 담았습니다.
(C++17 기준, Windows/MSVC 또는 Linux/g++ 환경에서 모두 동작)
Race Condition이 무엇이고 왜 발생하는지 벡터 삽입 예시로 이해한다.std::mutex로 상호배제(Mutual Exclusion) 를 구현해 충돌을 제거한다.unlock 누락/예외/조기 반환에도 안전하게 만든다.std::lock_guard vs std::unique_lock의 차이와 사용 위치를 익힌다.std::recursive_mutex의 존재와 주의점을 안다.g++ -std=c++17 -O2 -pthread main.cpp -o app
./app
vector에 동시 삽입#include <iostream>
#include <vector>
#include <thread>
using namespace std;
vector<int> vec;
void Push() {
for (int i = 0; i < 10'000; ++i) {
vec.push_back(i);
}
}
int main() {
thread t1(Push);
thread t2(Push);
t1.join(); t2.join();
cout << "vec.size() = " << vec.size() << '\n'; // 기대: 20000, 실제: 크래시 or 누락
}
std::vector는 스레드 안전하지 않다.push_back() 중 capacity 증가가 발생하면 새 메모리 할당 → 복사 → 기존 메모리 해제가 이뤄진다.reserve로도 완벽히 해결되지 않는 이유int main() {
vec.reserve(20000); // 재할당 회피
thread t1(Push);
thread t2(Push);
t1.join(); t2.join();
cout << "vec.size() = " << vec.size() << '\n'; // 여전히 20000이 아닐 수 있음
}
std::mutex로 상호배제#include <mutex>
vector<int> vec;
mutex m;
void Push() {
for (int i = 0; i < 10'000; ++i) {
m.lock();
vec.push_back(i);
m.unlock();
}
}
push_back()을 수행 → 안정성 확보.unlock() 누락(예: return/break/throw) 시 영구 대기(Deadlock) 위험.LockGuardtemplate <typename T>
class LockGuard {
public:
explicit LockGuard(T& m) : _mutex(&m) { _mutex->lock(); }
~LockGuard() { _mutex->unlock(); }
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
private:
T* _mutex;
};
void Push() {
for (int i = 0; i < 10'000; ++i) {
LockGuard<mutex> guard(m); // 생성 시 lock, 스코프 종료 시 자동 unlock
vec.push_back(i);
if (i == 5000) { // 예외/조기 반환/분기에도 자동 해제
// break; // 주석 해제해도 데드락 없음
}
}
}
std::lock_guard & std::unique_lockstd::lock_guard — 단순·가볍다void Push() {
for (int i = 0; i < 10'000; ++i) {
std::lock_guard<std::mutex> lock(m);
vec.push_back(i);
}
}
lock, 스코프 종료 시 자동 unlock.std::unique_lock — 유연하다void Push() {
for (int i = 0; i < 10'000; ++i) {
std::unique_lock<std::mutex> lk(m, std::defer_lock); // 아직 lock 안 함
// ... 선행 작업 ...
lk.lock(); // 원하는 지점에 lock
vec.push_back(i);
} // 소멸 시 자동 unlock
}
std::defer_lock : 생성 시 잠그지 않음 → 나중에 lock()std::try_to_lock: 바로 잠그기 시도, 실패해도 대기하지 않음std::adopt_lock : 이미 잠겨 있음을 가정(외부에서 lock한 뒤 인수로 넘길 때)
unique_lock은 내부 상태가 추가되어lock_guard보다 무겁지만 유연(move 가능 등).
for (...) {
std::lock_guard<std::mutex> lock(m);
vec.push_back(...);
}
{
std::lock_guard<std::mutex> lock(m);
for (...) {
vec.push_back(...);
}
}
팁:
std::chrono로 시간을 재어 자신의 워크로드에서 실제로 비교해보자.
std::recursive_mutex 개요std::mutex는 재귀 잠금 시 데드락이 날 수 있다.각 Step은 독립적으로 빌드/실행 가능하도록 작성했습니다.
아래에서 원하는 Step을 복사해main.cpp에 붙여넣고 실행하세요.
reserve(20000) 추가 후 실행 → 크래시는 사라져도 size 불일치 확인lock/unlock (위험 시나리오 체험)break 넣어 unlock 누락 → 프로그램이 끝나지 않음 확인LockGuard로 RAIILockGuard 클래스 추가lock_guard로 치환unique_lock + defer_lockrecursive_mutex 실험#include <mutex>
#include <iostream>
using namespace std;
recursive_mutex rm;
void Recur(int d) {
if (d == 0) return;
rm.lock();
cout << "depth=" << d << '\n';
Recur(d-1);
rm.unlock();
}
int main() {
Recur(3); // 정상 (mutex면 데드락)
}
1) std::vector에 동시 push_back이 위험한 두 가지 이유는?
2) reserve가 크래시를 줄여도 정확한 size를 보장하지 못하는 이유는?
3) lock/unlock 수동 관리의 가장 큰 위험은?
4) RAII가 예외 안전성을 제공하는 방식은?
5) lock_guard와 unique_lock의 차이점 두 가지를 적어라.
6) std::adopt_lock은 언제 쓰는가?
7) 락을 루프 안에서 잡을 때와 루프 밖에서 잡을 때의 트레이드오프는?
8) std::recursive_mutex가 필요한 경우와 주의점은?
std::mutex 기반 락이 필수unlock 누락을 원천 차단lock_guard, 유연하면 unique_lockdefer_lock/try_to_lock/adopt_lock 용도를 구분해 사용recursive_mutex, 하지만 남용 금지❌ unlock() 누락 → 영구 대기(Deadlock)
✅ RAII 사용 (lock_guard/unique_lock)
❌ 임계 구역에 느린 I/O(디스크/네트워크/콘솔) 넣기
✅ 임계 구역은 최소화, 계산/검증은 밖에서
❌ 여러 mutex를 서로 다른 순서로 잠그기
✅ 항상 고정된 순서로 잠그거나 std::scoped_lock/std::lock(다중 잠금) 사용
(강의 범위를 벗어나므로 참고만)
❌ 컨테이너를 공유 상태로 설계
✅ 스레드별 버퍼 → 마지막에 병합 같은 구조 고려
#include <bits/stdc++.h>
using namespace std;
vector<int> v;
mutex m;
void Push_lock_guard() {
for (int i = 0; i < 10'000; ++i) {
lock_guard<mutex> lk(m);
v.push_back(i);
}
}
void Push_unique_defer() {
for (int i = 0; i < 10'000; ++i) {
unique_lock<mutex> lk(m, defer_lock);
// ... 준비 작업 ...
lk.lock();
v.push_back(i);
}
}
int main() {
v.clear();
thread t1(Push_lock_guard);
thread t2(Push_unique_defer);
t1.join(); t2.join();
cout << "v.size() = " << v.size() << endl;
}
Q. std::atomic으로 vector를 보호할 수 없나요?
A. 원자적 load/store는 가능하지만, vector의 복잡한 내부 동작(재할당/복사/size 갱신)은 보호할 수 없습니다. mutex가 필요합니다.
Q. 왜 unique_lock이 더 무겁나요?
A. 상태를 더 많이 보유(소유/잠금 상태/소유권 이동 등)하기 때문입니다. 반대로 유연성이 장점입니다.
Q. 어디까지 락을 감싸야 하나요?
A. 데이터 무결성이 깨지지 않는 최소 범위를 기본으로 하되, 성능/경합을 측정해 조절합니다.
1) (a) 재할당 경합으로 해제된 메모리 접근 위험, (b) 재할당이 없어도 동시 인덱스 갱신 충돌(덮어쓰기/누락).
2) 동시 push_back이 같은 인덱스를 쓰며 덮어쓰기/누락이 발생.
3) unlock 누락 시 영구 대기. 예외/조기 반환에서도 누락될 수 있음.
4) 객체 소멸자에서 자동 unlock → 예외/return/break에도 해제 보장.
5) lock_guard: 가볍고 단순(즉시 lock, 이동 불가). unique_lock: 유연(지연 lock, move 가능, 상태 보유).
6) 외부에서 이미 lock()한 mutex를 소유권만 인계할 때(adopt_lock).
7) 루프-내: 병렬성↑/오버헤드↑. 루프-외: 오버헤드↓/병렬성↓. 상황에 맞게 선택.
8) 같은 스레드에서 중첩 잠금 필요 시 사용. 하지만 복잡도↑, 남용 금지.
컨테이너 동시 수정 = mutex 필수. lock_guard/unique_lock으로 RAII 적용, 락 범위는 의도적으로 선택!