Mutex and deadlock
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
we do
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.
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.
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
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: 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
we make
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:
-
Source code: deadlock.cpp