One way of preventing data races between the threads is to use mutexes.
A mutex is usually associated with a resource. The thread, which locks the mutex, has granted access to the resource. No other thread can then lock the mutex because it is already locked (look figure below). Consequently, no other thread has an access to the resource guarded by the locked mutex. This is the mutual exclusion: only one thread has access to the resource at any given time.
We already spoke about the problems which appear when using mutexes in our code:
remember Mutex and deadlock. There, we introduced
std::lock_guard class. But when we synchronized threads
with a condition variable, we used similar class:
std::unique_lock. What is the difference between these two
One of the differences between
std::unique_lock is that the programmer is able to unlock
std::unique_lock, but she/he is not able to unlock
std::lock_guard. Let’s explain it in more detail.
If you have an object
then the constructor of
guard1 locks the mutex. At the end of
guard1’s life, the destructor unlocks the mutex. There is no
other possibility. In fact, the
std::lock_guard class doesn’t
have any other member function.
On the other hand, we have an object of
There are similarities with
std::lock_guard class. The
guard2 also locks the mutex and the destructor
guard2 also unlocks the mutex. But the
std::unique_lock has additional functionalities.
The programmer is able to unlock the mutex with the help of the guard object
This means that the programmer can unlock the mutex before the
guard2’s life ends. After the mutex was unlocked, the
programmer can also lock it again
We should mention that the
std::unique_lock has also
some other member functions. You can look it up
When to use std::unique_lock ?
There are at least two reasons for using
std::unique_lock. Sometimes we are forced to use it: other
functions require it as an input. And other times using
std::unique_lock allows us to have more parallelizable code.
Let’s say that we have a long function. First part of the function accesses some shared resource and the second part locally processes the resource.
A mutex must be locked just in the first part of the function, because we access the element of the vector. In the second part, the mutex doesn’t need to be locked anymore (because we don’t access any shared variable).
In fact, it is preferable that the mutex is not locked in the second part, because then other threads can lock it. In principle, we would like that the locks last as little time as possible. This minimizes the time when threads are waiting to get a lock on the mutex and not doing any useful work. We obtain more parallelizable code.
Using functions that requires std::unique_lock
In Condition variable, we had to use
std::unique_lock as an input.
std::condition_variable::wait(...) unlocks the mutex and
waits for the
function call. Then,
wait(...) reacquires the lock and
We recognize that
wait(...) member function requires
std::unique_lock. The function can not use usual
std::lock_guard, because it unlocks/locks the mutex.
When to use std::lock_guard ?
std::unique_lock has all of the functionalities of the
std::lock_guard. Everything which is possible to do with
std::lock_guard is also possible to do with
std::unique_lock. So, when should we use
The rule of thumb is to always use
std::lock_guard. But if we
need some higher level functionalities, which are available by
std::unique_lock, then we should use the
We learned the differences between the
std::unique_lock. We also listed some situations where we
should use the