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
member function every time you locked a mutex. This must also happen in the case
Solution is the same as in joining the threads. We can use some sort of a Guard
object (recall the
class). Luckily, the C++ Standard
Library has it already. It is the
class. Therefore, instead
guard object will automatically unlock the mutex when
its destructor is called.
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.
The input of
makeAppleJuice is a bag which contains apples
"a") and pears (
We also have two mutexes:
m_print, which is associated to
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
"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.
The for loop removes all pears from the bag. A pear is removed from the bag by
changing the bag entry from
"x". Then, the function prints a messages. Of course, the
mutexes protect the operations.
The main function
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
But it might happen that you end up in an undesired situation.
What is happening here? This is best explained with a picture.
There are two resources:
bag. Both are protected by the two mutexes. The locks in the
picture represent the two mutexes.
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
At this point in time, both mutexes are locked. The picture symbolizes this when both threads reach the locks and lock them.
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.
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.
std::lockfunction can lock 2 or more mutexes at once without risk of a deadlock. Instead of having
The std::lock locks both mutexes. Therefore, we need to pass additional argument
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
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.
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.