Introduction
In multi-threaded programming, if multiple threads access the same data (shared resource) simultaneously, it can lead to data corruption or unexpected race conditions. To prevent this, we use exclusive control (locking) with std::mutex.
However, if you manually manage mutex.lock() and mutex.unlock(), there is a risk of deadlocks if a function throws an exception or returns early, causing unlock() to be skipped.
To solve this problem, C++ provides two classes based on the RAII (Resource Acquisition Is Initialization) principle: std::lock_guard and std::unique_lock. These classes acquire the lock when the object is created and automatically release it when the object goes out of scope.
1. std::lock_guard: Simple and Reliable Lock Management
std::lock_guard is the simplest lock management class with the least overhead. It provides basic scope-based exclusive locking functionality.
Features
- Locks the mutex in the constructor.
- Automatically unlocks the mutex in the destructor (when exiting the scope).
- Cannot manually unlock or transfer ownership of the lock in the middle of the scope.
Sample Code
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
// Mutex to protect shared resource (standard output in this example)
std::mutex mtx;
void print_message(const std::string& message) {
// mtx is locked as soon as the lock_guard object is created
std::lock_guard<std::mutex> guard(mtx);
// Only one thread can execute inside this block at a time
std::cout << message << " : Start" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // Simulate some heavy processing
std::cout << message << " : End" << std::endl;
// When exiting this function's scope, guard's destructor is called
// and mtx is automatically unlocked
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(print_message, "Thread " + std::to_string(i));
}
for (auto& th : threads) {
th.join();
}
return 0;
}
Explanation: In the print_message function, lock_guard ensures that unlock is called when exiting the scope, so you do not have to worry about forgetting to call lock/unlock.
2. std::unique_lock: More Flexible Lock Management
std::unique_lock provides a more advanced and flexible lock management mechanism in addition to the features of lock_guard.
Features
- Scope-based automatic lock/unlock, similar to
lock_guard. - Allows manual locking/unlocking (
.lock(),.unlock()). - Allows attempting to lock (
.try_lock()). - Can transfer (move) ownership of the lock.
Sample Code
#include <iostream>
#include <thread>
#include <mutex>
std::mutex resource_mutex;
void process_data(int id) {
// 1. Create unique_lock object without locking (defer_lock)
std::unique_lock<std::mutex> lock(resource_mutex, std::defer_lock);
std::cout << "Thread " << id << " is attempting to lock..." << std::endl;
// 2. Try to lock
if (lock.try_lock()) {
std::cout << "Thread " << id << " successfully locked." << std::endl;
// ... Processing using shared resource ...
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// 3. Manually unlock (if you want to release early)
lock.unlock();
std::cout << "Thread " << id << " released the lock." << std::endl;
} else {
std::cout << "Thread " << id << " failed to lock." << std::endl;
}
}
int main() {
std::thread t1(process_data, 1);
std::thread t2(process_data, 2);
t1.join();
t2.join();
return 0;
}
Explanation: unique_lock is a powerful tool when implementing complex locking strategies, such as using try_lock (returns true if locked, false if not).
Summary
| Feature | std::lock_guard | std::unique_lock |
| Basic Function | Scope-based automatic lock/unlock | Scope-based automatic lock/unlock |
| Simplicity | ◎ (Very Simple) | ◯ |
| Overhead | ◎ (Almost Zero) | △ (Slightly larger) |
| Manual Lock/Unlock | Not Possible | Possible |
| Try Lock (try_lock) | Not Possible | Possible |
| Ownership Transfer | Not Possible | Possible |
In conclusion, the best practice is to use the lighter and safer std::lock_guard when simple exclusive control is sufficient, and use std::unique_lock only when flexible control such as lock attempts or manual unlocking is required.
