Deadlock Problem:
Each of a pair of threads needs to lock both of a pair of mutexes to perform some operation, and each thread has one mutex and is waitingfor the other. Neither thread can proceed, because each is waiting for the other torelease its mutex. This scenario is called deadlock, and it’s the biggest problem withhaving to lock two or more mutexes in order to perform an operation.
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::lock(lhs.m,rhs.m);
std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock);
std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock);
swap(lhs.some_detail,rhs.some_detail);
}
};
First, the arguments are checked to ensure they are different instances, becauseattempting to acquire a lock on astd::mutex when you already hold it is undefinedbehavior. (A mutex that does permit multiple locks by the same thread is provided inthe form ofstd::recursive_mutex. See section 3.3.3 for details.) Then, the call tostd::lock()Blocks the two mutexes, and twostd::lock_guard instances are con-structed, one for each mutex. Thestd::adopt_lock parameter is supplied inaddition to the mutex to indicate to thestd::lock_guard objects that the mutexesare already locked, and they should just adopt the ownership of the existing lock onthe mutex rather than attempt to lock the mutex in the constructor.
This ensures that the mutexes are correctly unlocked on function exit in the gen-eral case where the protected operation might throw an exception; it also allows for asimple return. Also, it’s worth noting that locking either lhs.mor rhs.m inside the calltostd::lock can throw an exception; in this case, the exception is propagated outofstd::lock. Ifstd::lock has successfully acquired a lock on one mutex and an
exception is thrown when it tries to acquire a lock on the other mutex, this first lock isreleased automatically:std::lock provides all-or-nothing semantics with regard tolocking the supplied mutexes.
Flexible locking with std::unique_lock
std::unique_lock provides a bit more flexibility than std::lock_guard by relaxingthe invariants; a std::unique_lock instance doesn’t always own the mutex that it’sassociated with. First off, just as you can pass std::adopt_lock as a second argumentto the constructor to have the lock object manage the lock on a mutex, you can alsopass std::defer_lock as the second argument to indicate that the mutex shouldremain unlocked on construction. The lock can then be acquired later by callinglock() on the std::unique_lock object (not the mutex) or by passing the std::unique_lock object itself to std::lock(). Listing 3.6 could just as easily have beenwritten as shown in listing 3.9, using std::unique_lock and std::defer_lock Brather than std::lock_guard and std::adopt_lock. The code has the same linecount and is essentially equivalent, apart from one small thing: std::unique_locktakes more space and is a fraction slower to use than std::lock_guard. The flexibilityof allowing a std::unique_lock instance not to own the mutex comes at a price: thisinformation has to be stored, and it has to be updated.
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
private:
some_big_object some_detail;
std::mutex m;
public:
X(some_big_object const& sd):some_detail(sd){}
friend void swap(X& lhs, X& rhs)
{
if(&lhs==&rhs)
return;
std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock); //mutexes are not locked here
std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock);
std::lock(lock_a,lock_b); swap(lhs.some_detail,rhs.some_detail); // muteness are locked here
}
};
In listing 3.9, the std::unique_lock objects could be passed to std::lock() c becausestd::unique_lock provides lock(), try_lock(), and unlock() member functions.These forward to the member functions of the same name on the underlying mutexto do the actual work and just update a flag inside the std::unique_lock instance toindicate whether the mutex is currently owned by that instance. This flag is necessaryin order to ensure that unlock() is called correctly in the destructor. If the instancedoes own the mutex, the destructor must call unlock(), and if the instance does not ownthe mutex, it must not call unlock(). This flag can be queried by calling the owns_lock()member function.