C++11 concurrency: locks

In a previous post I introduced the C++11 support for threads. In this article I will discuss the locking features provided by the standard that one can use to synchronize access to shared resources.

The core syncing primitive is the mutex, which comes in four flavors, in the <mutex> header:

  • mutex: provides the core lock() and unlock() and the non-blocking try_lock() method that returns if the mutex is not available.
  • recursive_mutex: allows multiple acquisitions of the mutex from the same thread.
  • timed_mutex: similar to mutex, but it comes with two more methods try_lock_for() and try_lock_until() that try to acquire the mutex for a period of time or until a moment in time is reached.
  • recursive_timed_mutex: is a combination of timed_mutex and recusive_mutex.

Here is a simple example of using a mutex to sync the access to the std::cout shared object.

In the next example we’re creating a simple thread-safe container (that just uses std::vector internally) that has methods like add() and addrange(), with the later implemented by calling the first.

Running this program results in a deadlock.

The reason for the deadlock is that the tread that own the mutex cannot re-acquire the mutex, and such an attempt results in a deadlock. That’s were recursive_mutex come into picture. It allows a thread to acquire the same mutext multiple times. The maximum number of times is not specified, but if that number is reached, calling lock would throw a std::system_error. Therefore to fix this implementation (apart from changing the implementation of addrange not to call lock and unlock) is to replace the mutex with a recursive_mutex.

Then the output looks something like this:

Notice the same numbers are generated in each call to func(). That is because the seed is thread local, and the call to srand() only initializes the seed from the main thread. In the other worker threads it doesn’t get initialized, and therefore you get the same numbers every time.

Explicit locking and unlocking can lead to problems, such as forgetting to unlock or incorrect order of locks acquiring that can generate deadlocks. The standard provides several classes and functions to help with this problems.

The wrapper classes allow consistent use of the mutexes in a RAII-style with auto locking and unlocking within the scope of a block. These wrappers are:

  • lock_guard: when the object is constructed it attempts to acquire ownership of the mutex (by calling lock()) and when the object is destructed it automatically releases the mutex (by calling unlock()). This is a non-copyable class.
  • unique_lock: is a general purpose mutex wrapper that unlike lock_quard also provides support for deferred locking, time locking, recursive locking, transfer of lock ownership and use of condition variables. This is also a non-copyable class, but it is moveable.

With these wrappers we can rewrite the container class like this:

Notice that attempting to call try_lock_for() or try_lock_until() on a unique_lock that wraps a non-timed mutex results in a compiling error.

The constructors of these wrapper guards have overloads that take an argument indicating the locking strategy. The available strategies are:

  • defer_lock of type defer_lock_t: do not acquire ownership of the mutex
  • try_to_lock of type try_to_lock_t: try to acquire ownership of the mutex without blocking
  • adopt_lock of type adopt_lock_t: assume the calling thread already has ownership of the mutex

These strategies are declared like this:

Apart from these wrappers for mutexes, the standard also provides a couple of methods for locking one or more mutexes.

  • lock: locks the mutexes using a deadlock avoiding algorithm (by using calls to lock(), try_lock() and unlock()).
  • try_lock: tries to call the mutexes by calling try_lock() in the order of which mutexes were specified.

Here is an example of a deadlock case: we have a container of elements and we have a function exchange() that swaps one element from a container into the other container. To be thread-safe, this function synchronizes the access to the two containers, by acquiring a mutex associated with each container.

Suppose this function is called from two different threads, from the first, an element is removed from container 1 and added to container 2, and in the second it is removed from container 2 and added to container 1. This can lead to a deadblock (if the thread context switches from one thread to another just after acquiring the first lock).

To fix the problem, you can use std::lock that guaranties the locks are acquired in a deadlock-free way:

Hopefully this walktrough will help you understand the basics of the synchronization functionality supported in C++11.

Leave a Reply