The code of this section contains two pairs of functions that ought to be executed by concurrent threads, and that acquire two resources in form of mutexes. One pair provokes a deadlock and the other avoids it. In the main function, we are going to try them out:
- Let's first include all needed headers and declare that we use namespace std and chrono_literals:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
using namespace chrono_literals;
- Then we instantiate two mutex objects which we need in order to run into a deadlock:
mutex mut_a;
mutex mut_b;
- In order to provoke a deadlock with two resources, we need two functions. One function tries to lock mutex A and then mutex B, while the other function will do that in the opposite order. By letting both functions sleep a bit between the locks, we can make sure that this code blocks forever on a deadlock. (This is for demonstration purposes. A program without some sleep lines might run successfully without a deadlock sometimes if we start it repeatedly.)
Note that we do not use the 'n' character in order to print a line break, but we use endl. endl does not only perform a line break but also flushes the stream buffer of cout, so we can be sure that prints are not bunched up and postponed:
static void deadlock_func_1()
{
cout << "bad f1 acquiring mutex A..." << endl;
lock_guard<mutex> la {mut_a};
this_thread::sleep_for(100ms);
cout << "bad f1 acquiring mutex B..." << endl;
lock_guard<mutex> lb {mut_b};
cout << "bad f1 got both mutexes." << endl;
}
- As promised in the last step, deadlock_func_2 looks exactly same as deadlock_func_1, but it locks mutex A and B in the opposite order:
static void deadlock_func_2()
{
cout << "bad f2 acquiring mutex B..." << endl;
lock_guard<mutex> lb {mut_b};
this_thread::sleep_for(100ms);
cout << "bad f2 acquiring mutex A..." << endl;
lock_guard<mutex> la {mut_a};
cout << "bad f2 got both mutexes." << endl;
}
- Now we write a deadlock-free variant of those two functions we just implemented. They use the class scoped_lock, which locks all mutexes we provide as constructor arguments. Its destructor unlocks them again. While locking the mutexes, it internally applies a deadlock avoidance strategy for us. Note that both functions still use mutex A and B in opposite order:
static void sane_func_1()
{
scoped_lock l {mut_a, mut_b};
cout << "sane f1 got both mutexes." << endl;
}
static void sane_func_2()
{
scoped_lock l {mut_b, mut_a};
cout << "sane f2 got both mutexes." << endl;
}
- In the main function, we will go through two scenarios. First, we use the sane functions in multithreaded context:
int main()
{
{
thread t1 {sane_func_1};
thread t2 {sane_func_2};
t1.join();
t2.join();
}
- Then we use the deadlock-provoking functions that do not utilize any deadlock avoidance strategy:
{
thread t1 {deadlock_func_1};
thread t2 {deadlock_func_2};
t1.join();
t2.join();
}
}
- Compiling and running the program yields the following output. The first two lines show that the sane locking function scenario works and both functions return without blocking forever. The other two functions run into a deadlock. We can tell that this is a deadlock because we see the print lines that tell that the individual threads try to lock mutexes A and B and then wait forever. Both do not reach the point where they successfully locked both mutexes. We can let this program run for hours, days, and years, and nothing will happen.
This application needs to be killed from outside, for example by pressing the keys Ctrl + C:
$ ./avoid_deadlock
sane f1 got both mutexes
sane f2 got both mutexes
bad f2 acquiring mutex B...
bad f1 acquiring mutex A...
bad f1 acquiring mutex B...
bad f2 acquiring mutex A...