C++ Mutex: Complete Guide with Scenarios and Examples
C++ Mutex: Complete Guide with Scenarios and Examples
Mutex (mutual exclusion) is a fundamental synchronization primitive that ensures only one thread can access a shared resource at a time.
Table of Contents
- What is a Mutex?
- Mutex Types in C++
- Basic Usage
- Scenario 1: Protecting Shared Data
- Scenario 2: Critical Sections
- Scenario 3: Thread-Safe Counter
- Scenario 4: Singleton Pattern
- Scenario 5: Resource Pool
- Common Patterns and Best Practices
What is a Mutex?
A mutex provides:
- Exclusive access: Only one thread can hold the lock
- Blocking: Other threads wait until lock is released
- Thread safety: Prevents race conditions
Mutex Operations
lock(): Acquire lock (blocks if already locked)unlock(): Release locktry_lock(): Try to acquire (non-blocking)
Mutex Types in C++
1. std::mutex
Basic mutex, non-recursive:
#include <mutex>
using namespace std;
mutex mtx;
mtx.lock();
// Critical section
mtx.unlock();
2. std::recursive_mutex
Allows same thread to lock multiple times:
recursive_mutex rmtx;
rmtx.lock();
rmtx.lock(); // OK - same thread
rmtx.unlock();
rmtx.unlock();
3. std::timed_mutex
Mutex with timeout support:
timed_mutex tmtx;
if (tmtx.try_lock_for(chrono::seconds(1))) {
// Got lock
tmtx.unlock();
}
4. std::shared_mutex (C++17)
Allows shared (read) and exclusive (write) locks:
shared_mutex smtx;
shared_lock<shared_mutex> read_lock(smtx); // Multiple readers OK
unique_lock<shared_mutex> write_lock(smtx); // Exclusive writer
Basic Usage
RAII Lock Guards
#include <mutex>
using namespace std;
mutex mtx;
// GOOD: Automatic unlock
{
lock_guard<mutex> lock(mtx);
// Critical section
} // Automatically unlocked
// BAD: Manual lock/unlock
mtx.lock();
// If exception occurs, lock never released!
mtx.unlock();
Lock Types
lock_guard: Simple RAII wrapperunique_lock: More flexible (can unlock early, supports condition variables)scoped_lock(C++17): Lock multiple mutexes
Scenario 1: Protecting Shared Data
#include <vector>
#include <thread>
#include <mutex>
using namespace std;
class ThreadSafeVector {
private:
vector<int> data_;
mutex mtx_;
public:
void push(int value) {
lock_guard<mutex> lock(mtx_);
data_.push_back(value);
}
int get(size_t index) {
lock_guard<mutex> lock(mtx_);
return data_[index];
}
size_t size() {
lock_guard<mutex> lock(mtx_);
return data_.size();
}
};
Scenario 2: Critical Sections
class BankAccount {
private:
int balance_ = 0;
mutex mtx_;
public:
void deposit(int amount) {
lock_guard<mutex> lock(mtx_);
balance_ += amount;
}
bool withdraw(int amount) {
lock_guard<mutex> lock(mtx_);
if (balance_ >= amount) {
balance_ -= amount;
return true;
}
return false;
}
int getBalance() {
lock_guard<mutex> lock(mtx_);
return balance_;
}
};
Scenario 3: Thread-Safe Counter
class ThreadSafeCounter {
private:
int count_ = 0;
mutex mtx_;
public:
void increment() {
lock_guard<mutex> lock(mtx_);
++count_;
}
void decrement() {
lock_guard<mutex> lock(mtx_);
--count_;
}
int get() {
lock_guard<mutex> lock(mtx_);
return count_;
}
};
Scenario 4: Singleton Pattern
class Singleton {
private:
static Singleton* instance_;
static mutex mtx_;
Singleton() {}
public:
static Singleton* getInstance() {
if (instance_ == nullptr) {
lock_guard<mutex> lock(mtx_);
if (instance_ == nullptr) {
instance_ = new Singleton();
}
}
return instance_;
}
};
Singleton* Singleton::instance_ = nullptr;
mutex Singleton::mtx_;
Scenario 5: Resource Pool
template<typename T>
class ResourcePool {
private:
vector<T> resources_;
vector<bool> in_use_;
mutex mtx_;
public:
ResourcePool(size_t size) : resources_(size), in_use_(size, false) {}
optional<size_t> acquire() {
lock_guard<mutex> lock(mtx_);
for (size_t i = 0; i < in_use_.size(); ++i) {
if (!in_use_[i]) {
in_use_[i] = true;
return i;
}
}
return nullopt;
}
void release(size_t index) {
lock_guard<mutex> lock(mtx_);
in_use_[index] = false;
}
};
Common Patterns and Best Practices
1. Always Use RAII
// GOOD
lock_guard<mutex> lock(mtx);
// BAD
mtx.lock();
// ... if exception, never unlocked
mtx.unlock();
2. Lock Ordering
// GOOD: Always lock in same order
lock(mtx1, mtx2);
lock_guard<mutex> l1(mtx1, adopt_lock);
lock_guard<mutex> l2(mtx2, adopt_lock);
// BAD: Different order causes deadlock
thread1: lock(mtx1), lock(mtx2)
thread2: lock(mtx2), lock(mtx1) // Deadlock!
3. Minimize Lock Scope
// GOOD: Small critical section
{
lock_guard<mutex> lock(mtx);
shared_data = value;
}
expensiveOperation(); // Outside lock
// BAD: Large critical section
lock_guard<mutex> lock(mtx);
shared_data = value;
expensiveOperation(); // Blocks other threads unnecessarily
4. Avoid Nested Locks
// BAD: Potential deadlock
void func1() {
lock_guard<mutex> l1(mtx1);
func2(); // Also locks mtx1 - deadlock!
}
// GOOD: Use recursive_mutex if needed
recursive_mutex rmtx;
Summary
Mutex is essential for thread-safe programming:
- Protects shared data: Prevents race conditions
- Exclusive access: Only one thread at a time
- RAII wrappers: Use lock_guard, unique_lock
- Minimize scope: Keep critical sections small
- Lock ordering: Prevent deadlocks
By following these patterns, you can safely share data between threads in C++.