Beyond Locks, a Safer and More Expressive Way to Deal with Mutexes in C++
Today’s post is written by Louis-Charles Caron. Louis-Charles is a software engineer at Advanced Silicon, working on image processing for low latency human-machine interaction. Fan of coding, Louis-Charles enjoys programming in Python and C++ and likes to design tools to build faster, more intuitive software. He dived into multi-threading a couple of years ago and can no longer think sequentially!
I started writing multi-threaded code two years ago. Two years and one day ago, I would start sweating at the sound of the the words thread and mutex. But after literally a few days of practice (and, I admit, a 3-day course on multi-threading and C++11), I figured the basic principles are quite understandable.
Typical multi-thread problems can be solved by using a handful of tools. Admittedly, complex problems are even more complex when they appear in multi-threaded code, but I did not happen to run into those yet.
Since C++11, one now finds the necessary multi-threading tools in the C++ standard library. Finally! We can write multi-threaded code in pure C++.
The multi-threading part of the C++11 library is functional: it is simple and to the point. However, it is nearly impossible to write clear and expressive multi-threaded code using only the C++11 tools. And when multi-threaded code is not clear, it tends not to be safe.
In this article, I introduce some multi-threading tools you will find in the C++11 standard library through a code example. Although simple, this example will clearly demonstrate the shortcomings of the C++11 standard library. Then, I present safe: a small header-only C++11 library I designed to make my multi-threaded code more expressive, and ultimately safer.
Vocabulary
In this post, I use a mix of standard and homebrewed vocabulary. Here are the important terms I will use and their meaning:
- Value: whatever variable that needs to be protected for multi-threaded access. Examples:
int
,std::vector<float>
. - Mutex: an object that exhibits the
BasicLockable
interface:lock()
andunlock()
. Examples:std::mutex
,std::shared_mutex
(C++17). - Lock: an object that manages a mutex by the RAII idiom. Examples:
std::lock_guard
,std::shared_lock
(C++14).
Now, let’s dive into the multi-threaded code example!
Multi-threaded code example in C++11
In multi-threaded code, variables that are accessed by multiple threads must be protected if at least one thread modifies the variable. The simplest way to protect a variable in C++11 is by using an std::mutex
, and making sure the mutex is locked whenever the variable is accessed.
Locking and unlocking a mutex by hand is dangerous business though: forget to unlock it and the program is compromised. To ease the pain of manually locking and unlocking, C++11 provides lock objects like std::lock_guard
. std::lock_guard
’s job is simple: it locks a given mutex at construction and unlocks it upon destruction.
As long as the std::lock_guard
object lives, it is guaranteed that the mutex
is locked. Other lock objects, like std::unique_lock
, allow unlocking and relocking on demand and are useful in specific contexts (e.g. to use in conjunction with std::condition_variable
).
Needless to say, C++11 has a thread class, std::thread
, and signaling and protection mechanisms like std::condition_variable
and std::atomic
. These classes are an important part of the multi-threading standard library, but will not be treated in this article. Our only concern here is the difficulty to expressively use std::mutex
and the lock objects (like std::lock_guard
and std::unique_lock
).
The following example shows the basic usage of std::mutex
and std::lock_guard
, and some bad practices that might arise from their usage:
std::mutex fooMutex; std::mutex barMutex; std::string foo; // <-- do I need to lock a mutex to safely access this variable ? { std::lock_guard<std::mutex> lock(fooMutex); // <-- is this the right mutex ? foo = "Hello, World!"; } std::cout << foo << std::endl; // <-- unprotected access, is this intended ?
Good points
This example is all we need to analyze the usability of C++’s multi-threading classes:
- #1. Simple and clear. The standard classes are easy to use, each has a clear purpose and a focused public interface. Take
std::lock_guard
, for example. You can hardly find a simpler public interface: two constructors. Easy to use correctly, hard to misuse, indeed!
- #2. Customizable. Although simple, the classes have a few useful customization points. The locks can be used with any object with the
BasicLockable
interface, including your own mutex implementations. The locks’ behavior also is parameterizable by passing tags (likestd::adopt_lock
) at construction.
- #3. Shared mutexes and locks. C++14 and C++17 (and boost) introduced shared mutexes and shared locks. Shared mutexes and locks are an optimization for read-only pieces of multi-threaded code. It is totally safe for multiple threads to read the same variable, but
std::mutex
can not be locked by multiple threads simultaneously, even if those threads only want to read a value. Shared mutexes and locks allow this.
Bad points
- #1. It is not clear which variables in a piece of code are shared between several threads and thus need to be protected.
- #2. It is not clear which mutex is meant to protect which variable.
- In the example, only the name of the mutex (
fooMutex
) connects it to the value it protects (foo
). It feels very uncomfortable to rely on a variable’s name to enforce its correct usage!
- In the example, only the name of the mutex (
- #3. It is not clear whether accesses to the value are meant to be protected or not. Nothing warns the programmer about unprotected accesses to the value.
- At the end of the example,
foo
is accessed without locking the mutex. Is this an error from the programmer ? Or is it documented somewhere that at this particular point, the program is single threaded and the use of the mutex is not necessary ?
- At the end of the example,
- #4. Nothing prevents write accesses while using shared locking.
Observations
- #1. The mutex is locked for the lifetime of the lock object, and the value can safely be accessed within this time span. These two concepts (the locking/unlocking of the mutex and the possibility to access the value) should be tied to the lock’s lifetime, but the standard locks only take care of the mutex locking and unlocking.
- #2. Once created, the lock object sits there, waiting for its destruction to happen to unlock the mutex. Surely we can improve this poor lock’s life condition…
These are simple problems that can easily be fixed by a wrapper library. Let’s see one way to address these issues.
Introducing the safe library
safe is a small header-only library that aims to solve the problems in the usage of mutexes and locks in modern C++. Two class templates are at the code of the safe library. They encapsulate the value object, mutex and lock object to provide a more expressive interface:
- The
Lockable
class template packs a mutex and a value object together. The value object is accessible through theLockable
object using an expressive interface that clearly differentiate protected and unprotected access. - The
Access
class template aggregates a lock object and gives access to the value object through pointer semantics. As long as theAccess
object lives, the mutex is locked and the pointer-like access is possible. When theAccess
object is destroyed, the mutex is unlocked and the pointer-like access disappears.Const
access to the value can be enforced by theAccess
class template.
Before examining those two class templates in detail, let’s rewrite the code example using the safe library.
Multi-threaded code example using safe
Here is what the above example looks like when written using safe:
using LockableString = safe::Lockable<std::string>; // type aliases will save you a lot of typing std::mutex barMutex; LockableString foo; // <-- value and mutex packaged together! { safe::WriteAccess<LockableString> fooAccess(foo); // <-- right mutex: guaranteed! *fooAccess = "Hello, World!"; // access the value using pointer semantics: * and -> } // from here, you cannot directly access the value anymore: jolly good, since the mutex is not locked anymore! std::cout << foo.unsafe() << std::endl; // <-- unprotected access: clearly expressed!
The Lockable
class template
The Lockable
class template basically lets you store any value and mutex together. Additionally, a Lockable object gives access to the value object in 3 expressive ways: ReadAccess
objects, WriteAccess
objects and the unsafe()
member function.
I am pretty sure you can figure the use of each of these constructs. The unsafe()
member function simply returns a reference to the value object. You may use this function when you know you are in a single threaded context (e.g. within a constructor). ReadAccess
and WriteAccess
are type aliases used to easily construct read-only and read-write Access
objects. The Access
class template is described in the next section.
The Lockable
class takes care of the first 3 problems:
- It is clear that the value inside a
Lockable
must be protected for multi-threaded access, otherwise, you would not stick it inside aLockable
object! - The value and the lockable are clearly associated within the same object.
- The protected and unprotected accesses are obtained by functions with different names, and to obtain unprotected access, you literally have to type the word: “unsafe”. I dare you not be warned!
The Access class template
You can see an Access
object is a combination of a lock and a pointer to the value. Access
objects are meant to be constructed from Lockable
objects. The Access
object will lock the Lockable
’s mutex and expose its value object.
Using the Access
class template, you enjoy the power of RAII with the added benefit that the RAII concept is extended to also include the ability to access the value object.
According to observation #1, the lifetime of the RAII lock object, the locking and unlocking of the mutex and the possibility to access the value should be tied together. This is exactly what Access
objects do. And observation #2 is also addressed because the access object is used throughout its lifetime to access the value. If you access a value object through an Access
object, your accesses are guaranteed to be thread-safe. Disclaimer: if you unlock the mutex during the lifetime of the Access object, then the previous statement does not hold!
The declaration of the Access
class template is:
template<template<typename> class LockType, AccessMode Mode> class Access;
The first template parameter lets you choose the type of lock you want to use (locks are class templates, which is why the LockType
parameter is a template itself!).
The second template parameter has to do with the const
ness of the access to the value object. The parameter can take two values: ReadWrite
or ReadOnly
. Access objects with ReadOnly
template parameter only allow const access to the value object. This solves problem #4, as you can use the ReadOnly
mode in conjunction with shared mutexes and shared locks to enforce read-only access to the value.
Highlights of safe
- Much safer and expressive than pure C++
- Clearly identify the value objects that need to be protected.
- Clearly associate the mutex with the values objects they protect.
- Clearly distinguish protected and unprotected accesses.
- Prevent unwanted unprotected accesses.
- Simple and easy to use
- Simply replace your mutex by
Lockable
objects and locks byAccess
objects.
- Simply replace your mutex by
- Customizable
- Use any mutex and lock type! The library is written in C++11, but you can use C++17’s
std::shared_mutex
if you want! - Use standard tags to customize the behavior or your
Access
objects. - Customize the read-write or read-only behavior of your
Access
objects.
- Use any mutex and lock type! The library is written in C++11, but you can use C++17’s
- Shared mutex friendly
- Enforce read-only access when using shared mutexes.
Downsides of safe
safe code is a bit more verbose than standard C++ code because Lockable
objects are templated both of the value and on the mutex type. When creating an Access
object, you add two more template parameters. That leads to a lot of typing. When using safe, type aliases truly are your friend!
Summary
C++11 gave us the tools to write multi-threaded code in pure C++. However, it did not quite give us the tools to write expressive multi-threaded code. Unfortunately, in multi-threading more than anywhere else, clarity is the basis for safety.
In this article, I pointed out the shortcomings of the C++11 standard library for multi-threading through a simple code example. To avoid the drawbacks of the standard library, I introduced safe. safe is a small header-only library that builds upon the C++11 tools to enable writing clearer and safer multi-threaded code.
If you write multi-threaded code in modern C++, I really encourage you to give safe a try. You will find my code on github. I put a lot of heart into safe’s design and implementation, I hope you will like it. Feedback is more than welcome.
safe is a clone!
When I had the idea for safe, I did not care to look around to see if it already existed. I just went on and coded it the way I wanted it.
Since then, I did some research and found many other implementations. Two of them are major: Synchronized
(from folly), and synchronized_value
(from boost). folly’s version seems to be particularly well written. There are two important differences between safe and these implementations:
- Both are part of large libraries while safe is a standalone header-only library. Start writing safer programs by adding one single line to your code:
#include "safe/lockable.h"
! - boost’s synchronized_value, and also folly’s Synchronized, but to a lesser extent, gave their synchronized class value semantics. That is, the class is designed to behave as the underlying type (the Value type) in many situations. I do not like value semantics, especially for safe. Using a Lockable object has a cost, and I want that cost to be visible. It is the whole point of the library to make it apparent that the value is protected by a mutex and value semantics blurs the picture in my opinion.
How do you make your multi-threaded code expressive?
Do you use an external library to wrap the C++ standard primitives?
Don't want to miss out ? Follow:   Share this post!