Bad locking:
std::mutex mtx;
void bad_lock_example() {
mtx.lock();
try
{
foo();
bar();
if (baz()) {
mtx.unlock(); // Have to unlock on each exit point.
return;
}
quux();
mtx.unlock(); // Normal unlock happens here.
}
catch(...) {
mtx.unlock(); // Must also force unlock in the presence of
throw; // exceptions and allow the exception to continue.
}
}
That is the wrong way to implement the locking and unlocking of the mutex. To ensure the correct release of the mutex with unlock()
requires the programer to make sure that all the flows resulting in the exiting of the function result in a call to unlock()
. As shown above this is a brittle processes as it requires any maintainers to continue following the pattern manually.
Using an appropriately crafted class to implement RAII, the problem is trivial:
std::mutex mtx;
void good_lock_example() {
std::lock_guard<std::mutex> lk(mtx); // constructor locks.
// destructor unlocks. destructor call
// guaranteed by language.
foo();
bar();
if (baz()) {
return;
}
quux();
}
lock_guard
is an extremely simple class template that simply calls lock()
on its argument in its constructor, keeps a reference to the argument, and calls unlock()
on the argument in its destructor. That is, when the lock_guard
goes out of scope, the mutex
is guaranteed to be unlocked. It doesn't matter if the reason it went out of scope is an exception or an early return - all cases are handled; regardless of the control flow, we have guaranteed that we will unlock correctly.