C++ mutex for Race Conditions — Complete Guide
이 글의 핵심
Fix C++ data races with std::mutex, lock_guard, unique_lock, and scoped_lock. Deadlock avoidance, shared_mutex for readers/writers, and production patterns for thread-safe code.
Introduction: Why did the counter break?
“Order counts don’t match the database”
On event day, multiple worker threads incremented one shared counter. After the batch, totals were tens of thousands off from the DB.
Cause: plain int counter + counter++ from many threads → lost updates (data race). Fix: std::atomic
// 실행 예제 sequenceDiagram participant T1 as Thread 1 participant M as mutex participant T2 as Thread 2 T1->>M: lock() M-->>T1: acquired T2->>M: lock() Note over T2: blocked T1->>M: critical section T1->>M: unlock() M-->>T2: acquired T2->>M: critical section
After reading:
- Tell race condition vs data race
- Protect sections with std::mutex, lock_guard, unique_lock
- Avoid deadlock with ordering /
std::lock/scoped_lock - Use RAII so locks release on exceptions
Table of contents
- Race condition and data race
- Bug scenarios
- std::mutex usage
- lock_guard and unique_lock
- Avoiding deadlock
- Common mistakes
- Performance notes
- Best practices
- Production patterns
- Patterns
1. Race condition and data race
Race condition: outcome depends on scheduling order. Data race (C++): concurrent unsynchronized access where at least one is a write → undefined behavior. Mutex serializes access to shared data so read-modify-write sequences don’t interleave incorrectly.
2. Bug scenarios (summary)
- Stock / check-then-act: guard both check and update with one lock
- Log buffer:
string +=from many threads → protect with mutex - Work queue:
emptythenfront/popmust be one atomic operation → mutex + oftencondition_variable - Hit/miss counters: update related stats under one lock for consistent snapshots
- Config map: concurrent read/write on
std::map→shared_mutexor external mutex
3. std::mutex — basic usage
// Paste: g++ -std=c++17 -pthread -o mutex_safe mutex_safe.cpp && ./mutex_safe
#include <mutex>
#include <thread>
#include <iostream>
int counter = 0;
std::mutex counter_mutex;
void safeIncrement() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(counter_mutex);
++counter;
}
}
int main() {
std::thread t1(safeIncrement);
std::thread t2(safeIncrement);
t1.join();
t2.join();
std::cout << "counter=" << counter << "\n";
return 0;
}
Prefer lock_guard / unique_lock over raw lock()/unlock() so exceptions can’t leave the mutex locked.
try_lock: non-blocking attempt; if you lock, you must unlock (RAII still best).
4. lock_guard and unique_lock
RAII: lock in constructor, unlock in destructor.
unique_lock: can unlock()/lock() mid-scope—needed for condition_variable.
scoped_lock (C++17): lock multiple mutexes deadlock-free:
std::scoped_lock lock(m1, m2);
5. Avoiding deadlock
Cause: two threads lock A then B vs B then A. Fixes:
- Global lock order (always A then B)
- std::lock(lock1, lock2) with
defer_lock+unique_lock - std::scoped_lock(m1, m2) (recommended on C++17+)
- Minimize critical section—no I/O inside locks
6. Common mistakes
returnbeforeunlockwith manual lock/unlock → use RAII- Mutex far from data → easy to access data unlocked → encapsulate
- Double-lock same
std::mutexin one thread → deadlock (non-recursive) - Callbacks under lock → can re-enter or block on other locks → call callbacks outside the lock
7. Performance
| Approach | When |
|---|---|
mutex | Multiple variables, invariants across fields |
atomic | Single counters/flags, no invariants with other vars |
| Lock-free | Expert-only; hard to get right |
| Keep lock duration minimal; don’t hold mutexs across I/O. |
8. Best practices
- Prefer RAII (
lock_guard,unique_lock,scoped_lock) - Encapsulate data + mutex in a type
- Use
shared_mutexwhen reads dominate - Use ThreadSanitizer:
g++ -fsanitize=thread -g
9. Production patterns
- Thread-safe queue wrapper: all methods lock internally
- shared_mutex:
shared_lockfor read,unique_lockfor write - Snapshot publish:
atomic_store/atomic_loadofshared_ptr<const Data>for rare writes
10. Patterns
Bundle data + mutex in one struct; expose only thread-safe methods.
Implementation checklist
- Shared state protected by mutex or atomic
- RAII locks
- Minimal critical sections
- Multi-lock:
scoped_lockor fixed order -
find/remove+eraseidiom for containers
Related posts
- std::thread basics
- Atomics
- RAII
Keywords
C++ mutex, lock_guard, unique_lock, shared_mutex, scoped_lock, race condition, deadlock, critical section, data race
Summary
- Data race ⇒ UB; synchronize with mutex/atomic/correct algorithms
- lock_guard for simple scopes; unique_lock for
condition_variableor mid-scope unlock - scoped_lock for multiple mutexes
- Deadlocks: consistent ordering or
std::lock/scoped_lockNext: condition_variable
FAQ
When is this useful?
A. Every multi-threaded C++ program with shared mutable data—servers, games, UI backends.
Read first?
A. Thread basics, then series index.
One-line summary: Wrap shared data in mutex + RAII; avoid deadlocks with scoped_lock and lock order discipline.
References
Related posts
- Thread basics
- Advanced threading
- condition_variable
- Atomics
- Stack vs heap
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Data Race | ‘Mutex 대신 Atomic을 써야 하는 상황은?’ 면접 단골 질문 정리
- C++ std::thread 입문 | join 누락·디태치 남용 등 자주 하는 실수 3가지와 해결법
- C++ 멀티스레드 Asio의 딜레마 | Data Race와 Mutex의 한계 [#2]
이 글에서 다루는 키워드 (관련 검색어)
C++, Mutex, std::mutex, lock_guard, unique_lock, shared_mutex, scoped_lock 등으로 검색하시면 이 글이 도움이 됩니다.