[2026] C++ Mutex & Lock — Mutual Exclusion, lock_guard, unique_lock, and Deadlock Prevention
이 글의 핵심
A practical guide to C++ mutex and locks: mutex basics, lock_guard (RAII), unique_lock, multi-mutex locking, and real-world pitfalls.
Introduction
A mutex is a synchronization primitive for mutual exclusion. It stops multiple threads from accessing shared data at the same time and helps prevent data races.
What you actually see in production
When you learn to program, examples are tidy and theoretical. Production is different: legacy code, tight schedules, and bugs you did not expect. The topics here were learned as theory first; applying them in real projects is when you see why APIs are shaped the way they are.
What stuck with me was a rough patch on an early project. I did what the book said, but it still failed, and I spent days on it. A senior’s code review surfaced the issue, and I learned a lot from that. This article covers not only the mechanics, but traps and fixes you are likely to hit in practice.
1. std::mutex basics
Basic usage
#include <mutex>
#include <thread>
#include <iostream>
// std::mutex: mutual exclusion — only one thread at a time
std::mutex mtx;
int sharedData = 0; // shared data accessed by multiple threads
void increment() {
// mtx.lock(): lock the mutex (other threads block until unlock)
mtx.lock();
// Critical section: at most one thread runs this at a time
++sharedData;
// mtx.unlock(): release the mutex so another thread can enter
mtx.unlock();
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "sharedData: " << sharedData << std::endl; // 2
return 0;
}
Problem: exception safety
#include <mutex>
#include <stdexcept>
#include <iostream>
std::mutex mtx;
void unsafeFunction() {
mtx.lock();
// If an exception is thrown, unlock never runs!
if (someCondition) {
throw std::runtime_error("error");
}
mtx.unlock(); // not reached if an exception is thrown
}
int main() {
try {
unsafeFunction();
} catch (...) {
std::cout << "Exception: mutex stays locked!" << std::endl;
}
return 0;
}
The core issue: If unsafeFunction throws before unlock, the mutex can remain locked forever. Threads that later wait on the same mutex can stall in a deadlock-like state.
2. lock_guard (RAII)
Automatic lock and unlock
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx;
int sharedData = 0;
void safeIncrement() {
std::lock_guard<std::mutex> lock(mtx);
++sharedData;
// Destructor unlocks automatically
}
int main() {
std::thread t1(safeIncrement);
std::thread t2(safeIncrement);
t1.join();
t2.join();
std::cout << "sharedData: " << sharedData << std::endl; // 2
return 0;
}
Pattern: lock_guard locks in the constructor and unlocks in the destructor, so release is guaranteed even on return or exception. Keeping the guard’s lifetime as small as possible shrinks the critical section and reduces contention.
Exception safety
#include <mutex>
#include <stdexcept>
#include <iostream>
std::mutex mtx;
void safeFunction() {
std::lock_guard<std::mutex> lock(mtx);
// Even if this throws, unlock runs in the destructor
if (someCondition) {
throw std::runtime_error("error");
}
// Destructor of lock calls unlock
}
int main() {
try {
safeFunction();
} catch (...) {
std::cout << "Exception: mutex released automatically" << std::endl;
}
return 0;
}
3. unique_lock
Flexible locking
#include <mutex>
#include <iostream>
std::mutex mtx;
void flexibleLock() {
std::unique_lock<std::mutex> lock(mtx);
std::cout << "work 1" << std::endl;
lock.unlock();
std::cout << "work without lock" << std::endl;
lock.lock();
std::cout << "work 2" << std::endl;
// Destructor unlocks if still held
}
int main() {
flexibleLock();
return 0;
}
Deferred locking
#include <mutex>
#include <iostream>
std::mutex mtx;
void deferredLock() {
// Do not lock in the constructor
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
if (needLock) {
lock.lock();
}
// perform work
}
int main() {
deferredLock();
return 0;
}
4. Multiple mutexes
Deadlock risk
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;
// Bad: deadlock possible
void badTransfer() {
// Thread 1: mtx1 then mtx2
mtx1.lock();
mtx2.lock();
balance1 -= 50;
balance2 += 50;
mtx2.unlock();
mtx1.unlock();
}
void anotherBadTransfer() {
// Thread 2: mtx2 then mtx1 (deadlock!)
mtx2.lock();
mtx1.lock();
balance2 -= 30;
balance1 += 30;
mtx1.unlock();
mtx2.unlock();
}
Using std::lock
#include <mutex>
#include <thread>
#include <iostream>
std::mutex mtx1, mtx2;
int balance1 = 100, balance2 = 200;
// Good: std::lock avoids deadlock
void safeTransfer() {
std::lock(mtx1, mtx2); // lock both atomically
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 << "transfer done: " << 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 — concise multi-mutex RAII
void modernTransfer() {
std::scoped_lock lock(mtx1, mtx2);
std::cout << "performing transfer" << std::endl;
// Destructor unlocks both
}
int main() {
std::thread t1(modernTransfer);
std::thread t2(modernTransfer);
t1.join();
t2.join();
return 0;
}
5. Mutex types
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: recursive lock on same mutex
}
int main() {
func2();
return 0;
}
timed_mutex
#include <mutex>
#include <thread>
#include <iostream>
std::timed_mutex tmtx;
void tryLockFor() {
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
std::cout << "lock acquired" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
tmtx.unlock();
} else {
std::cout << "lock failed (timeout)" << std::endl;
}
}
int main() {
std::thread t1(tryLockFor);
std::thread t2(tryLockFor);
t1.join();
t2.join();
return 0;
}
6. Practical example: threads
Analogy: Concurrency is like one cook switching between tasks on several dishes. Parallelism is several cooks working on different dishes at once.
Thread-safe counter
#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;
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 << "final count: " << counter.get() << std::endl; // 10000
return 0;
}
Summary
Takeaways
- mutex: mutual exclusion for shared data
- lock_guard: simple RAII lock
- unique_lock: flexible lock with manual control
- scoped_lock: multiple mutexes (C++17)
- recursive_mutex: re-entrant locking on the same thread
- timed_mutex: try-lock with a time limit
Lock comparison
| Lock | Characteristics | Overhead | When to use |
|---|---|---|---|
| lock_guard | Simple, RAII | Low | Default choice |
| unique_lock | Flexible, manual control | Medium | Conditional locking, std::condition_variable |
| scoped_lock | Multiple mutexes | Low | Multiple mutexes (C++17) |
Practical tips
Guidelines:
- Prefer
lock_guardby default. - Use
unique_lockwhen you need manual lock/unlock or condition variables. - For several mutexes, prefer
scoped_lock(C++17). - Use
recursive_mutexonly when re-entrant locking is truly required.
Deadlock avoidance:
- Use a consistent global order for locking mutexes.
- Use
std::lockfor atomic multi-lock. - Prefer
scoped_lock(C++17). - Keep critical sections short.
Performance:
- Keep critical sections minimal.
lock_guardis usually the cheapest.- Use
unique_lockonly when necessary. - For read-heavy workloads, consider
shared_mutex(C++17).
Next steps
- C++ atomics
- C++ condition variables
- C++ threads
Related posts
- C++ async & launch
- C++ atomic operations