C++ Reactive Programming
上QQ阅读APP看书,第一时间看更新

Avoiding deadlock

While dealing with mutexes, the biggest problem that can arise is a deadlock. To understand what deadlock is, just imagine an iPod. For an iPod to achieve its purpose, it requires both an iPod as well as an earpiece. If two siblings share one iPod, there are situations where both want to listen to music at the same time. Imagine one person got their hands on the iPod and the other got the earpiece, and neither of them is willing to share the item they possess. Now they are stuck, unless one of them tries to be nice and lets the other person listen to music.

Here, the siblings are arguing over an iPod and an earpiece, but coming back to our situation, threads argue over the locks on mutexes. Here, each thread has one mutex and is waiting for the other. No mutex can proceed here, because each thread is waiting for the other thread to release its mutex. This scenario is called deadlock.

Avoiding deadlock is sometimes quite straightforward because different mutexes serve different purposes, but there are instances where handling such situations is not that obvious. The best advice I can give you to avoid deadlock is to always lock multiple mutexes in the same order. Then, you will never get deadlock situations.

Consider an example of a program with two threads; each thread is intended to print odd numbers and even numbers alone. Since the intentions of the two threads are different, the program uses two mutexes to control each thread. The shared resource between the two threads is std::cout. Let's look at the following program with a deadlock situation:

// Global mutexes 
std::mutex evenMutex; 
std::mutex oddMutex;  
// Function to print even numbers 
void printEven(int max) 
{ 
    for (int i = 0; i <= max; i +=2) 
    { 
        oddMutex.lock(); 
        std::cout << i << ","; 
        evenMutex.lock(); 
        oddMutex.unlock(); 
        evenMutex.unlock(); 
    } 
} 

The printEven() function is defined to print all the positive even numbers into the standard console which are less than the max value. Similarly, let us define a printOdd() function to print all the positive odd numbers less than max, as follows:

// Function to print odd numbers 
void printOdd(int max) 
{ 
    for (int i = 1; i <= max; i +=2) 
    { 
        evenMutex.lock(); 
        std::cout << i << ","; 
        oddMutex.lock(); 
        evenMutex.unlock(); 
        oddMutex.unlock(); 
         
    } 
} 

Now, let's write the main function to spawn two independent threads to print odd and even numbers using the previously defined functions as the thread functions for each operation:

int main() 
{ 
    auto max = 100; 
     
    std::thread t1(printEven, max); 
    std::thread t2(printOdd, max); 
     
    if (t1.joinable()) 
        t1.join(); 
    if (t2.joinable()) 
        t2.join(); 
} 

In this example, std::cout is protected with two mutexes, printEven and printOdd, which perform locking in a different order. With this code, we always ends up in deadlock, since each thread is clearly waiting for the mutex locked by the other thread. Running this code would result in a hang. As mentioned previously, deadlock can be avoided by locking them in the same order, as follows:

void printEven(int max) 
{ 
    for (int i = 0; i <= max; i +=2) 
    { 
        evenMutex.lock(); 
        std::cout << i << ","; 
        oddMutex.lock(); 
        evenMutex.unlock(); 
        oddMutex.unlock(); 
    } 
}  
void printOdd(int max) 
{ 
    for (int i = 1; i <= max; i +=2) 
    { 
        evenMutex.lock(); 
        std::cout << i << ","; 
        oddMutex.lock(); 
        evenMutex.unlock(); 
        oddMutex.unlock(); 
         
    } 
} 

But this code is clearly not clean. You already know that using a mutex with the RAII idiom makes the code cleaner and safer, but to ensure the order of locking, the C++ standard library has introduced a new function, std::lock—a function that can lock two or more mutexes in one go without deadlock risk. The following example shows how to use this for our previous odd-even program:

void printEven(int max) 
{ 
    for (int i = 0; i <= max; i +=2) 
    { 
        std::lock(evenMutex, oddMutex); 
        std::lock_guard<std::mutex> lk_even(evenMutex, std::adopt_lock); 
        std::lock_guard<std::mutex> lk_odd(oddMutex, std::adopt_lock); 
        std::cout << i << ","; 
    } 
}  
void printOdd(int max) 
{ 
    for (int i = 1; i <= max; i +=2) 
    { 
        std::lock(evenMutex, oddMutex); 
        std::lock_guard<std::mutex> lk_even(evenMutex, std::adopt_lock); 
        std::lock_guard<std::mutex> lk_odd(oddMutex, std::adopt_lock); 
         
        std::cout << i << ","; 
         
    } 
} 

In this case, as soon as the thread execution enters the loop, the call to std::lock locks the two mutexes. Two std::lock_guard instances are constructed for each mutex. The std::adopt_lock parameter is supplied in addition to the mutex instance to std::lock_guard to indicate that the mutexes are already locked, and they should just adopt the ownership of the existing lock on the mutex rather than attempt to lock the mutex in the constructor. This guarantees safe unlocking, even in exceptional cases.

However, std::lock can help you to avoid deadlocks in cases where the program demands the locking of two or more mutexes at the same time; it doesn't help if they are acquired separately. Deadlocks are one of the hardest problems that can occur in a multithreaded program. It ultimately relies on the discipline of a programmer to not get into any deadlock situations.