A mutex is very powerful tool for a concurrent programming. We learned about it in the last article. But inappropriate use of mutexes can lead to serious problems. We will explore two of the most common problems.

Forgetting to unlock

The first problem is similar to the problem of spawning a thread and then joining/detaching it. You must remember to call .unlock() member function every time you locked a mutex. This must also happen in the case of exceptions.

Solution is the same as in joining the threads. We can use some sort of a Guard object (recall the Guard class). Luckily, the C++ Standard Library has it already. It is the std::lock_guard class. Therefore, instead

std::mutex m;
...
{
    m.lock();
    ...
    // some code
    ...
    m.unlock();
}
...

we do

std::mutex m;
...
{
    std::lock_guard<std::mutex> guard(m);
    ...
    // some code
    ...
}
...

and the guard object will automatically unlock the mutex when its destructor is called.

Deadlock

Let’s start to explore the deadlock with an example:

John is picking up fruits at his garden and he ends up with a lot of fruits in a bag. Unfortunately, he finds out that all of the pears are poisoned because of a naughty neighbor. Therefore the pears must be thrown out! However, the apples are ok, so he decides to make an apple juice out of them using a grandmothers famous recipe. The recipe requires at least 5 apples to make a refreshing juice.

Because John is a programmer, he decides to write a computer program, that will “make the juice”. Additionally, he notices that tasks: making the juice and throwing away the pears could be done concurrently. Let’s look through the John’s code.

std::mutex m_print;
std::mutex m_bag;

void makeAppleJuice(std::vector<std::string>& bag)
{
    std::lock_guard<std::mutex> printGuard(m_print);
    std::cout << "Making an apple juice..." << std::endl;

    std::lock_guard<std::mutex> bagGuard(m_bag);
    
    int numApples = 0;
    for (std::size_t i = 0; i != bag.size(); i++)
    {
        if (bag[i] == "a")
        {
            numApples++;
            bag[i] = "x";
        }
    }
    
    if (numApples < 5)
    {
        std::cout << "I can not make an apple juice." << std::endl;
    }
    else
    {
        std::cout << "I made an excellent apple juice for you." << std::endl;
    }
}

The input of makeAppleJuice is a bag which contains apples ("a") and pears ("p").

We also have two mutexes: m_print, which is associated to printing, and m_bag which is associated to the modification of the bag.

The function prints some information and removes the apples from the bag. (An apple is removed by changing the bag entry from "a" to "x"). Additionally, function counts the number of apples and makes the juice only if the number of apples is greater than 5.

The code for throwing out the pears is similar.

void throwOutPear(std::vector<std::string>& bag)
{
    std::lock_guard<std::mutex> bagGuard(m_bag);
    
    for (std::size_t i = 0; i != bag.size(); i++)
    {
        if (bag[i] == "p")
        {
            bag[i] = "x";
        }
    }
    
    std::lock_guard<std::mutex> printGuard(m_print);
    std::cout << "I threw out all pears from you bag!" << std::endl;    
}

The for loop removes all pears from the bag. A pear is removed from the bag by changing the bag entry from "p" to "x". Then, the function prints a messages. Of course, the mutexes protect the operations.

The main function

void printItem(const std::string& fruit)
{
    std::cout << fruit << " ";
}


int main()
{
    std::vector<std::string> bag = {"a", "a", "a", "p", "p", "a", "p", "a"};
    
    std::thread t1(makeAppleJuice, std::ref(bag));
    std::thread t2(throwOutPear, std::ref(bag));
    t1.join();
    t2.join();

    std::cout << "Bag: ";    
    std::for_each(bag.begin(), bag.end(), printItem);
    std::cout << std::endl;
    
    return 0;
}

creates a bag and two threads for the two tasks. Finally, it also prints the content of the bag.

But when we try to run it, we might get an unexpected behavior. If everything goes well, the output is as following

$ ./deadlock 
Making an apple juice...
I made an excellent apple juice for you.
I threw out all pears from you bag!
Bag: x x x x x x x x 

But it might happen that you end up in an undesired situation.

$ ./deadlock 
Making an apple juice...

What is happening here? This is best explained with a picture.

Deadlock

There are two resources: std::cout and bag. Both are protected by the two mutexes. The locks in the picture represent the two mutexes.

The thread t1 runs the function makeAppleJuice and locks the mutex associated with printing. At the same time, the thread t2 runs the function throwOutPear an it locks the mutex associated with the bag.

At this point in time, both mutexes are locked. The picture symbolizes this when both threads reach the locks and lock them.

But t1 would now like to lock m_bag. The mutex is already locked, so t1 can not lock it. Therefore, t1 waits for m_bag to be unlocked.

Similarly, t2 would like to lock m_print. But it is already locked. Therefore, t2 also waits for m_print to be unlocked. Both threads are waiting for each other. And they will be waiting forever. The situation, when two threads are cyclically waiting on each other, is called a deadlock.

How to avoid deadlocks?

There are many techniques that allow us to avoid the deadlocks.

  • The simplest solution is to always lock the mutexes in the same order.

  • The std::lock function can lock 2 or more mutexes at once without risk of a deadlock. Instead of having

std::lock_guard<std::mutex> bagGuard(m_bag);
std::lock_guard<std::mutex> printGuard(m_print);
  

we make

std::lock(m_print, m_bag);
std::lock_guard<std::mutex> printGuard(m_print, std::adopt_lock);
std::lock_guard<std::mutex> bagGuard(m_bag, std::adopt_lock);

The std::lock locks both mutexes. Therefore, we need to pass additional argument to the std::lock_guard constructor, which tells that the mutex has already been locked and the lock_guard doesn’t need to lock it again.

  • Never wait a thread if there is a chance that it is waiting for you.

  • Don’t lock a mutex if you already locked another mutex. If you need to do it, do it with std::lock.

  • Generalization of locking the mutexes in the same order is to have a hierarchy of mutexes. Then you only lock a mutex if it is high/low enough in the hierarchy.

Summary

We explored two problems regarding mutexes: forgetting to unlock and having a deadlock. The deadlock is a problem when two threads are waiting for each other indefinitely. Then, we looked at the techniques of avoiding deadlocks. The simplest technique is to always lock the mutexes in the same order.

In the next article, we will look how can we return a value from a thread.

Links: