Deadlock Avoidance
-
Make the threads acquire locks in the same order
- Relies on the programmer
-
Lock multiple mutexes in a single operation
- Thread A locks mut1 and mut2
- Thread B cannot lock mut2 or mut1 during this operation
- A much better solution
-
C++ provides library features for this
std::scoped_lock
-
C++17 has
std::scored_lock -
Very similar to
std::lock_guard- Except it can lock more than one mutex at the same time
std::scoped_lock scope_lck(mut1, mut2...);
- Except it can lock more than one mutex at the same time
-
The mutexes are locked in the order given in the constructor call
- In the destructor, the mutexes are unlocked in the reverse order
-
This avoids the possibility of deadlock with multiple mutexes
#include <iostream>
#include <mutex>
#include <thread>
using namespace std::literals;
std::mutex mut1;
std::mutex mut2;
void funcA() {
std::cout << "Thread A trying to lock mutexes 1 and 2...\n";
std::scoped_lock scoped_lck(mut1, mut2);
std::cout << "Thread A has locked mutexes 1 and 2\n";
std::this_thread::sleep_for(50ms);
std::cout << "Thread A releasing mutexes 1 and 2...\n";
}
void funcB() {
std::cout << "Thread B trying to lock mutexes 2 and 1...\n";
std::scoped_lock scoped_lck(mut2, mut1);
std::cout << "Thread B has locked mutexes 2 and 1\n";
std::this_thread::sleep_for(50ms);
std::cout << "Thread B releasing mutexes 2 and 1...\n";
}
int main() {
std::thread thrA(funcA);
std::thread thrB(funcB);
thrA.join();
thrB.join();
}
std::scoped_lock Caveat
-
scoped_lockcan be used with a single mutexstd::scoped_lock scoped_lck(mut); -
It is easy to accidentally omit the argument
std::scoped_lock scoped_lck; -
This will compile and run, but not actually perform any locking
- May cause an unexpected data race
Deadlock Avoidance before C++17
- Use the
std::lockfunction- It can lock multiple mutexes in a single operation
// Lock two mutexes std::lock(mut1, mut2);
Adopting Locks
std::unique_lock can "adopt" the locks
- Pass the
std::adopt_lockoption to its constructor - Then
std::unique_lockobject now owns the lock
void funcA() {
std::cout << "Thread A trying to lock mutexes 1 and 2...\n";
std::lock(mut1, mut2);
// Each lock is adopted by a unique_lock object
std::unique_lock<std::mutex> uniq_lk1(mut1, std::adopt_lock);
std::unique_lock<std::mutex> uniq_lk2(mut2, std::adopr_lock);
std::cout << "Thread A has adopted the locks\n";
std::this_thread::sleep_for(50ms);
std::cout << "Thread A releasing mutexes 1 and 2...\n";
}
Deferring Locks
- Alternatively, we can "defer" the locking
- Pass the
std::defer_lockoption to the constructor - Then lock the mutexes later
- Pass the
void funcA()
// Each unique_lock object is associated with a mutex
std::unique_lock<std::mutex> uniq_lk1(mut1, std::defer_lock);
std::unique_lock<std::mutex> uniq_lk2(mut2, std::defer_lock);
std::cout << "Thread A trying to lock mutexes 1 and 2...\n";
std::lock(uniq_lk1, uniq_lk2);
std::cout << "Thread A trying has locked mutexes 1 and 2...\n";
std::this_thread::sleep_for(50ms);
std::cout << "Thread A releasing mutexes 1 and 2...\n";
std::try_lock()
-
Also locks multiple mutexes in a single operation
-
Returns immediately if it cannot obtain all the locks
- On failure, it returns the index of the object that failed to lock (0 for the first argument, etc)
-
On success, it returns -1
void funcA() {
std::unique_lock<std::mutex> uniq_lk1(mut1, std::defer_lock);
std::unique_lock<std::mutex> uniq_lk2(mut2, std::defer_lock);
std::cout << "Thread A trying to lock mutexes 1 and 2...\n";
auto idx = std::try_lock(uniq_lk1, uniq_lk2);
if (idx != -1) {
std::cout << "try_lock failed on mutex with index " << idx << '\n';
} else {
std::cout << "Thread A has locked mutexes 1 and 2\n";
std::this_thread::sleep_for(50ms);
std::cout << "Thread A releasing mutexes 1 and 2...\n";
}
}
Hierarchical Mutex
-
Sometimes this approach is not suitable
- It is not feasible to acquire multiple locks simultaneaously
-
A common technique is to impose an ordering
-
A thread cannot lock a mutex unless it has already locked a mutex with a lower status
- ID number
- Alphabetical name
-
The Williams book has a hierarchical_mutex that implements this
Deadlock Avoidance Guidelines
-
Avoid waiting for a thread while holding a lock
- The other thread may need the lock to proceed
-
Try to avoid waiting for other threads
- The other thread may be waiting for your thread
-
Try to avoid nested locks
- If your thread already holds a lock, do not acquire another one
- If you need multiple locks, acquire them in a single operation
-
Avoid calling functions within a critical section
- Unless you are certain the function does not try to lock