This lesson explains shared data and locks used in C++ for concurrency. The lesson covers both theory and practical part of this concept in detail with the help of interactive code examples.

To make the point clear, you only need to think about synchronization if you have shared, mutable data because shared, mutable data is prone to data races. If you have concurrent non-synchronised read and write access to data, your program has undefined behavior.

The easiest way to visualize concurrent, unsynchronized read and write operations is to write something to std::cout.

Let’s have a look.

Press + to interact
// coutUnsynchronised.cpp
#include <chrono>
#include <iostream>
#include <thread>
class Worker{
public:
Worker(std::string n):name(n){};
void operator() (){
for (int i = 1; i <= 3; ++i){
// begin work
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// end work
std::cout << name << ": " << "Work " << i << " done !!!" << std::endl;
}
}
private:
std::string name;
};
int main(){
std::cout << std::endl;
std::cout << "Boss: Let's start working.\n\n";
std::thread herb= std::thread(Worker("Herb"));
std::thread andrei= std::thread(Worker(" Andrei"));
std::thread scott= std::thread(Worker(" Scott"));
std::thread bjarne= std::thread(Worker(" Bjarne"));
std::thread bart= std::thread(Worker(" Bart"));
std::thread jenne= std::thread(Worker(" Jenne"));
herb.join();
andrei.join();
scott.join();
bjarne.join();
bart.join();
jenne.join();
std::cout << "\n" << "Boss: Let's go home." << std::endl;
std::cout << std::endl;
}

The program describes a workflow. The boss has six workers (lines 29 - 34). Each worker has to take care of 3 work packages. The work package takes 1/5 second (line 13). After the worker is done with his work package, he screams out loudly to the boss (line 15). Once the boss receives notifications from all workers, he sends them home (line 43).

What a mess for such a simple workflow.

The most straightforward solution is to use a mutex.

Mutexes

Mutex stands for mutual exclusion. It ensures that only one thread can access a critical section at any one time.

By using a mutex, the mess of the workflow turns into a harmony.

Press + to interact
// coutSynchronised.cpp
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex coutMutex;
class Worker{
public:
Worker(std::string n):name(n){};
void operator() (){
for (int i = 1; i <= 3; ++i){
// begin work
std::this_thread::sleep_for(std::chrono::milliseconds(200));
// end work
coutMutex.lock();
std::cout << name << ": " << "Work " << i << " done !!!" << std::endl;
coutMutex.unlock();
}
}
private:
std::string name;
};
int main(){
std::cout << std::endl;
std::cout << "Boss: Let's start working." << "\n\n";
std::thread herb= std::thread(Worker("Herb"));
std::thread andrei= std::thread(Worker(" Andrei"));
std::thread scott= std::thread(Worker(" Scott"));
std::thread bjarne= std::thread(Worker(" Bjarne"));
std::thread bart= std::thread(Worker(" Bart"));
std::thread jenne= std::thread(Worker(" Jenne"));
herb.join();
andrei.join();
scott.join();
bjarne.join();
bart.join();
jenne.join();
std::cout << "\n" << "Boss: Let's go home." << std::endl;
std::cout << std::endl;
}

std::cout is protected by the coutMutex in line 8. A simple lock() in line 19 and the corresponding unlock() call in line 21 ensure that the workers won’t scream all at once.

🔑 std::cout is thread-safe

The C++11 standard guarantees that you must not protect std::cout. Each character will be written atomically. It is possible that more output statements like those in the example will interleave. This is only a visual issue; the program is well-defined. This remark is valid for all global stream objects. Insertion to and extraction from global stream objects (std::cout, std::cin, std::cerr, and std::clog) is thread-safe.

To put it more formally: writing to std::cout is not a data race but a race condition. This means that the result depends on the interleaving of threads.

C++ has five different mutexes that can lock recursively, tentative with and without time constraints. With C++14 we have a std::shared_timed_mutex that is the base for reader-writer locks.

Method mutex recursive_mutex timed_mutex recursive_timed_mutex shared_timed_mutex
m.lock yes yes yes yes yes
m.unlock yes yes yes yes yes
m.try_lock yes yes yes yes yes
m.try_lock_for yes yes yes
m.try_lock_until yes yes yes
m.try_lock_shared yes yes
m.try_lock_shared_for yes
m.try_lock_shared_until yes

The std::shared_timed_mutex enables you to implement reader-writer locks. This means you can use std::shared_timed_mutex for exclusive or for shared locking. You will get an exclusive lock, if you put the std::shared_timed_mutex into a std::lock_guard; you will get a shared lock, if you put the std::shared_timed_mutex into a std::unique_lock. The method m.try_lock_for(relTime) (m.try_lock_shared_for(relTime)) needs a relative time duration; the method m.try_lock_until(absTime) (m.try_lock_shared_until(absTime)) an absolute time point.

m.try_lock (m.try_lock_shared) tries to lock the mutex and returns immediately. On success, it returns true, otherwise false. In contrast the methods try_lock_for (try_lock_shared_for) and try_lock_until (try_lock_shared_until) try to lock until the specified timeout occurs or the lock is acquired, whichever comes first. You should use a steady clock for your time constraint. A steady clock cannot be adjusted.

i std::shared_mutex with C++17

With C++17 we get a new mutex: std::shared_mutex. std::shared_mutex is similar to std::shared_timed_mutex. According to std::shared_timed_mutex, you can use it for exclusive or shared locking, but you can not specify a time point or a time duration.

You should not use mutexes directly; you should put mutexes into locks. Here is the reason why.

Issues of Mutexes

The issues with mutexes boil down to one main concern: deadlocks.

Deadlock: A deadlock is a state where two or more threads are blocked because each thread waits for the release of a resource before it releases its own resource.

The result of a deadlock is a total standstill. The thread that tries to acquire the resource, and usually the whole program is blocked forever. It is easy to produce a deadlock. Curious?

Exceptions and Unknown Code

The small code snippet has a lot of issues.

Press + to interact
std::mutex m;
m.lock();
sharedVariable = getVar();
m.unlock();

Here are the issues:

  1. If the function getVar() throws an exception, the mutex m will not be released.

  2. Never ever call an unknown function while holding a lock. If the function getVar tries to lock the mutex m, the program has undefined behavior because m is not a recursive mutex. Most of the time, the undefined behavior will result in a deadlock.

  3. Avoid calling a function while holding a lock. Maybe the function is from a library and you get a new version of the library, or the function is rewritten. There is always the danger of a deadlock.

The more locks your program needs, the more challenging it becomes. The dependency is very non-linear.

Lock Mutexes in Different Order

Here is a typical scenario of a deadlock resulting from locking in a different order.

widget

Thread 1 and thread 2 need access to two resources in order to finish their work. The problem arises when the requested resources are protected by two separate mutexes and are requested in different orders (Thread 1: Lock 1, Lock 2; Thread 2: Lock 2, Lock 1). In this case, the thread executions will interleave in such a way that thread 1 gets mutex 1, then thread 2 gets mutex 2, and we reach a standstill. Each thread wants to get the other’s mutex, but to get the other mutex the other thread has to release it first. The expression “deadly embrace” describes this kind of deadlock very well.

It’s easy to translate this picture into code.

Press + to interact
// deadlock.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
struct CriticalData{
std::mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
a.mut.lock();
std::cout << "get the first mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
b.mut.lock();
std::cout << "get the second mutex" << std::endl;
// do something with a and b
a.mut.unlock();
b.mut.unlock();
}
int main(){
CriticalData c1;
CriticalData c2;
std::thread t1([&]{deadLock(c1,c2);});
std::thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
}

Threads t1 and t2 call deadlock (lines 12 - 23). The function deadlock needs variables CriticalData c1 and c2 (lines 27 and 28). Because objects c1 and c2 have to be protected from shared access, they internally hold a mutex (to keep this example short and simple, CriticalData doesn’t have any other methods or members apart from a mutex).

A short sleep of about 1 millisecond in line 16 is sufficient to produce the deadlock.

The only choice left is to press CTRL+C and kill the process.

Locks will not solve all the issues with mutexes but will come to the rescue in many cases.

Locks

Locks take care of their resource following the RAII idiom. A lock automatically binds its mutex in the constructor and releases it in the destructor. This considerably reduces the risk of a deadlock, because the runtime takes care of the mutex.

Locks are available in three different flavours: std::lock_guard for the simple use-cases; std::unique-lock for the advanced use-cases. std::shared_lock is available with C++14 and can be used to implement reader-writer locks.

std::lock_guard

First, the simple use-case.

Press + to interact
std::mutex m;
m.lock();
sharedVariable = getVar();
m.unlock();

The mutex m ensures that access to the critical section sharedVariable= getVar() is sequential. Sequential means in this special case that each thread gains access to the critical section after the other. This maintains a kind of total order in the system. The code is simple but prone to deadlocks. A deadlock appears if the critical section throws an exception or if the programmer simply forgets to unlock the mutex. With std::lock_guard we can do this in a more elegant way.:

Press + to interact
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}

That was easy, but what’s the story with the opening and closing brackets? The lifetime of std::lock_guard is limited by the curly brackets. This means that its lifetime ends when it passes the closing curly brackets. Exactly at that point in time, the std::lock_guard destructor is called and - as you may have guessed - the mutex is released. This happens automatically and, it happens if getVar() in sharedVariable = getVar() throws an exception. The function scope and loop scope also limit the lifetime of an object.

i std::scoped_lock with C++17

With C++17 we get a std::scoped_lock. It’s very similar to std::unique_lock, but std::scoped_lock can lock an arbitrary number of mutexes atomically. You have to keep two facts in mind.

  1. If one of the current threads already owns the corresponding mutex and the mutex is not recursive, the behaviour is undefined.

  2. You can only take the ownership of the mutex without locking them. In this case, you have to provide the std::adopt_lock_t flag to the constructor: std::scoped_lock(std::adopt_lock_t, MutexTypes& ... m).

You can quite elegantly solve the previous deadlock by using a std::scoped_lock. I will discuss the resolution of the deadlock in the following section.

A std::unique_lock is stronger but more expensive than its little brother std::lock_guard.

std::unique_lock

In addition to what’s offered by a std::lock_guard, a std::unique_lock enables you to

  • create it without an associated mutex.

  • create it without locking the associated mutex.

  • explicitly and repeatedly set or release the lock of the associated mutex.

  • move the mutex.

  • try to lock the mutex.

  • delay the lock on the associated mutex.

The following table shows the methods of a std::unique_lock lk.

Method Description
lk.lock() Locks the associated mutex.
std::lock(lk1, lk2, ... ) Locks atomically the arbitrary number of associated mutexes.
lk.try_lock() and lk.try_lock_for(relTime) and lk.try_lock_until(absTime) Tries to lock the associated mutex.
lk.release() Release the mutex. The mutex remains locked.
lk.swap(lk2) and std::swap(lk, lk2) Swaps the locks.
lk.mutex() Returns a pointer to the associated mutex.
lk.owns_lock() Checks if the lock has a mutex.

lk.try_lock_for(relTime) needs a relative time duration; lk.try_lock_until(absTime) an absolute time point.

lk.try_lock tries to lock the mutex and returns immediately. On success, it returns true, otherwise false. In contrast the methods lk.try_lock_for and lk.try_lock_until block until the specified timeout occurs or the lock is acquired, whichever comes first. You should use a steady clock for your time constraint. A steady clock cannot be adjusted.

The method lk.release() returns the mutex; therefore, you have to unlock it manually.

Thanks to std::unique_lock, it is quite easy to lock many mutexes in one atomic step. Therefore you can overcome deadlocks by locking mutexes in a different order. Remember the deadlock from the subsection Issues of Mutexes?

Press + to interact
// deadlock.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
struct CriticalData{
std::mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
a.mut.lock();
std::cout << "get the first mutex" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
b.mut.lock();
std::cout << "get the second mutex" << std::endl;
// do something with a and b
a.mut.unlock();
b.mut.unlock();
}
int main(){
CriticalData c1;
CriticalData c2;
std::thread t1([&]{deadLock(c1,c2);});
std::thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
}

Let’s solve the issue. The function deadLock has to lock its mutexes atomically and that’s exactly what happens in the following example.

Press + to interact
// deadlockResolved.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
using namespace std;
struct CriticalData{
mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
unique_lock<mutex> guard1(a.mut,defer_lock);
cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;
this_thread::sleep_for(chrono::milliseconds(1));
unique_lock<mutex> guard2(b.mut,defer_lock);
cout << " Thread: " << this_thread::get_id() << " second mutex" << endl;
cout << " Thread: " << this_thread::get_id() << " get both mutex" << endl;
lock(guard1,guard2);
// do something with a and b
}
int main(){
cout << endl;
CriticalData c1;
CriticalData c2;
thread t1([&]{deadLock(c1,c2);});
thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
cout << endl;
}

If you call the constructor of std::unique_lock with std::defer_lock, the underlying mutex will not be locked automatically. At this point (lines 16 and 21), the std::unique_lock is just the owner of the mutex. Thanks to the variadic template std::lock, the lock operation is performed in an atomic step (line 25). A variadic template is a template which can accept an arbitrary number of arguments. std::lock tries to get all locks in one atomic step, so it either gets all of them or none of them and retries until it succeeds.

In this example, std::unique_lock manages the lifetime of the resources and std::lock locks the associated mutex. You can do it the other way around. In the first step the mutexes are locked, in the second std::unique_lock manages the lifetime of resources. Here is an example of the second approach.

Press + to interact
std::lock(a.mut, b.mut);
std::lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
std::lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);

Let us see this approach in action:

Press + to interact
// deadlockResolved.cpp
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
using namespace std;
struct CriticalData{
mutex mut;
};
void deadLock(CriticalData& a, CriticalData& b){
lock_guard<std::mutex> guard1(a.mut, std::adopt_lock);
cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;
this_thread::sleep_for(chrono::milliseconds(1));
lock_guard<std::mutex> guard2(b.mut, std::adopt_lock);
cout << " Thread: " << this_thread::get_id() << " second mutex" << endl;
cout << " Thread: " << this_thread::get_id() << " get both mutex" << endl;
lock(a.mut, b.mut);
// do something with a and b
}
int main(){
cout << endl;
CriticalData c1;
CriticalData c2;
thread t1([&]{deadLock(c1,c2);});
thread t2([&]{deadLock(c2,c1);});
t1.join();
t2.join();
cout << endl;
}

i Resolving the deadlock with a std::scoped_lock

With C++17, the resolution of the deadlock becomes quite easy. We get the std::scoped_lock that can lock an arbitrary number of mutexes atomically. You only have to use a std::lock_guard instead of the std::lock call. That’s all. Here is the modified function deadlock.

// deadlockResolvedScopedLock.cpp

... 
void deadLock(CriticalData& a, CriticalData& b){
  
  cout << "Thread: " << this_thread::get_id() << " first mutex" << endl;
  this_thread::sleep_for(chrono::milliseconds(1));
  cout << "  Thread: " << this_thread::get_id() << " second mutex" <<  endl;
  cout << "    Thread: " << this_thread::get_id() << " get both mutex" << endl;
  
  std::scoped_lock(a.mut, b.mut);
  // do something with a and b
}

...

With C++14, C++ adds support for std::shared_lock.

std::shared_lock

A std::shared_lock has the same interface as a std::unique_lock but behaves differently when used with a std::shared_timed_mutex. Many threads can share one std::shared_timed_mutex and, therefore, implement a reader-writer lock. The idea of reader-writer locks is straightforward and extremely useful. An arbitrary number of threads executing read operations can access the critical region at the same time, but only one thread is allowed to write.

Reader-writer locks do not solve the fundamental problem - threads competing for access to a critical region, but they do help to minimize the bottleneck.

A telephone book is a typical example using a reader-writer lock. Usually, a lot of people want to look up a telephone number, but only a few want to change them. Let’s look at an example.

Press + to interact
// readerWriterLock.cpp
#include <iostream>
#include <map>
#include <shared_mutex>
#include <string>
#include <thread>
std::map<std::string,int> teleBook{{"Dijkstra", 1972}, {"Scott", 1976},
{"Ritchie", 1983}};
std::shared_timed_mutex teleBookMutex;
void addToTeleBook(const std::string& na, int tele){
std::lock_guard<std::shared_timed_mutex> writerLock(teleBookMutex);
std::cout << "\nSTARTING UPDATE " << na;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
teleBook[na]= tele;
std::cout << " ... ENDING UPDATE " << na << std::endl;
}
void printNumber(const std::string& na){
std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
std::cout << na << ": " << teleBook[na];
}
int main(){
std::cout << std::endl;
std::thread reader1([]{ printNumber("Scott"); });
std::thread reader2([]{ printNumber("Ritchie"); });
std::thread w1([]{ addToTeleBook("Scott",1968); });
std::thread reader3([]{ printNumber("Dijkstra"); });
std::thread reader4([]{ printNumber("Scott"); });
std::thread w2([]{ addToTeleBook("Bjarne",1965); });
std::thread reader5([]{ printNumber("Scott"); });
std::thread reader6([]{ printNumber("Ritchie"); });
std::thread reader7([]{ printNumber("Scott"); });
std::thread reader8([]{ printNumber("Bjarne"); });
reader1.join();
reader2.join();
reader3.join();
reader4.join();
reader5.join();
reader6.join();
reader7.join();
reader8.join();
w1.join();
w2.join();
std::cout << std::endl;
std::cout << "\nThe new telephone book" << std::endl;
for (auto teleIt: teleBook){
std::cout << teleIt.first << ": " << teleIt.second << std::endl;
}
std::cout << std::endl;
}

The telephone book in line 9 is the shared variable, which has to be protected. Eight threads want to read the telephone book, two threads want to modify it (lines 31 - 40). To access the telephone book at the same time, the reading threads use the std::shared_lock<std::shared_timed_mutex>> in line 23. This is in contrast to the writing threads, which need exclusive access to the critical section. The exclusivity is given by the std::lock_guard<std::shared_timed_mutex>> in line 15. At the end the program displays the updated telephone book (lines 55 - 58).

The output of the reading threads overlaps, while the writing threads are executed one after the other. This means that the reading operations are performed at the same time.

That was easy. Too easy. The telephone book has undefined behavior

Undefind behaviour

The program has undefined behavior. To be more precise it has a data race. What? Before you continue, stop for a few seconds and think. By the way the concurrent access to std::cout is not the issue.

The characteristic of a data race is that at least two threads access the shared variable at the same time and at least one of them is a writer. This exact scenario may occur during program execution. One of the features of the ordered associative container is that reading of the container can modify it. This happens if the element is not available in the container. If “Bjarne” is not found in the telephone book, a pair (“Bjarne”,0) will be created from the read access. You can simply force the data race by putting the printing of Bjarne in line 40 in front of all the threads (lines 31 - 40). Let’s have a look.

You can see it right at the top, Bjarne has the value 0.

Press + to interact
// readerWriterLock.cpp
#include <iostream>
#include <map>
#include <shared_mutex>
#include <string>
#include <thread>
std::map<std::string,int> teleBook{{"Dijkstra", 1972}, {"Scott", 1976},
{"Ritchie", 1983}};
std::shared_timed_mutex teleBookMutex;
void addToTeleBook(const std::string& na, int tele){
std::lock_guard<std::shared_timed_mutex> writerLock(teleBookMutex);
std::cout << "\nSTARTING UPDATE " << na;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
teleBook[na]= tele;
std::cout << " ... ENDING UPDATE " << na << std::endl;
}
void printNumber(const std::string& na){
std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
std::cout << na << ": " << teleBook[na];
}
int main(){
std::cout << std::endl;
std::thread reader8([]{ printNumber("Bjarne"); });
std::thread reader1([]{ printNumber("Scott"); });
std::thread reader2([]{ printNumber("Ritchie"); });
std::thread w1([]{ addToTeleBook("Scott",1968); });
std::thread reader3([]{ printNumber("Dijkstra"); });
std::thread reader4([]{ printNumber("Scott"); });
std::thread w2([]{ addToTeleBook("Bjarne",1965); });
std::thread reader5([]{ printNumber("Scott"); });
std::thread reader6([]{ printNumber("Ritchie"); });
std::thread reader7([]{ printNumber("Scott"); });
reader1.join();
reader2.join();
reader3.join();
reader4.join();
reader5.join();
reader6.join();
reader7.join();
reader8.join();
w1.join();
w2.join();
std::cout << std::endl;
std::cout << "\nThe new telephone book" << std::endl;
for (auto teleIt: teleBook){
std::cout << teleIt.first << ": " << teleIt.second << std::endl;
}
std::cout << std::endl;
}

An obvious way to fix this issue is to use only reading operations in the function printNumber:

Press + to interact
// readerWriterLock.cpp
#include <iostream>
#include <map>
#include <shared_mutex>
#include <string>
#include <thread>
std::map<std::string,int> teleBook{{"Dijkstra", 1972}, {"Scott", 1976},
{"Ritchie", 1983}};
std::shared_timed_mutex teleBookMutex;
void addToTeleBook(const std::string& na, int tele){
std::lock_guard<std::shared_timed_mutex> writerLock(teleBookMutex);
std::cout << "\nSTARTING UPDATE " << na;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
teleBook[na]= tele;
std::cout << " ... ENDING UPDATE " << na << std::endl;
}
void printNumber(const std::string& na){
std::shared_lock<std::shared_timed_mutex> readerLock(teleBookMutex);
auto searchEntry = teleBook.find(na);
if(searchEntry != teleBook.end()){
std::cout << searchEntry->first << ": " << searchEntry->second << std::endl;
}
else {
std::cout << na << " not found!" << std::endl;
}
}
int main(){
std::cout << std::endl;
std::thread reader8([]{ printNumber("Bjarne"); });
std::thread reader1([]{ printNumber("Scott"); });
std::thread reader2([]{ printNumber("Ritchie"); });
std::thread w1([]{ addToTeleBook("Scott",1968); });
std::thread reader3([]{ printNumber("Dijkstra"); });
std::thread reader4([]{ printNumber("Scott"); });
std::thread w2([]{ addToTeleBook("Bjarne",1965); });
std::thread reader5([]{ printNumber("Scott"); });
std::thread reader6([]{ printNumber("Ritchie"); });
std::thread reader7([]{ printNumber("Scott"); });
reader1.join();
reader2.join();
reader3.join();
reader4.join();
reader5.join();
reader6.join();
reader7.join();
reader8.join();
w1.join();
w2.join();
std::cout << std::endl;
std::cout << "\nThe new telephone book" << std::endl;
for (auto teleIt: teleBook){
std::cout << teleIt.first << ": " << teleIt.second << std::endl;
}
std::cout << std::endl;
}

If a key is not in telephone book, I will simply write not found to the console.

You can see the message Bjarne not found! in the output of the second program execution. In the first program execution, addToTeleBook will be executed first; therefore, Bjarne will be found.

Thread-safe Initialization

If the variable is never modified there is no need for synchronization by using an expensive lock or an atomic. You only have to ensure that it is initialized in a thread-safe way.

There are three ways in C++ to initialize variables in a thread-safe way.

  • constant expressions.
  • the function std::call_once in combination with the flag std::once_flag.
  • a static variable with block scope.

🔑 Thread-safe initialisation in the main-thread

The easiest and fourth way to initialise a variable in a thread-safe way: initialise the variable in the main-thread before you create any child threads.

Constant Expressions

Constant expressions are expressions that the compiler can evaluate at compile time. They are implicitly thread-safe. Placing the keyword constexpr in front of a variable makes the variable a constant expression. The constant expression must be initialized immediately.

Press + to interact
constexpr double pi = 3.14;

In addition, user-defined types can also be constant expressions. For those types, there are a few restrictions that must be met in order to initialize it at compile time.

  • They must not have virtual methods or a virtual base class.

  • Their constructor must be empty and itself be a constant expression.

  • Their methods, which should be callable at compile time, must be constant expressions.

Instances of MyDouble satisfy all these requirements. So it is possible to instantiate them at compile time. This instantiation is thread-safe.

Press + to interact
// constexpr.cpp
#include <iostream>
class MyDouble{
private:
double myVal1;
double myVal2;
public:
constexpr MyDouble(double v1,double v2):myVal1(v1),myVal2(v2){}
constexpr double getSum() const { return myVal1 + myVal2; }
};
int main() {
constexpr double myStatVal = 2.0;
constexpr MyDouble myStatic(10.5, myStatVal);
constexpr double sumStat= myStatic.getSum();
std::cout << "SumStat: "<<sumStat << std::endl;
}

std::call_once and std::once_flag

By using the std::call_once function you can register a callable. The std::once_flag ensures that only one registered function will be invoked. You can register additional functions via the same std::once_flag. Only one function from that group is called.

std::call_once obeys the following rules:

  • Exactly one execution of exactly one of the functions is performed. It is undefined which function will be selected for execution. The selected function runs in the same thread as the std::call_once invocation it was passed to.

  • No invocation in the group returns before the above-mentioned execution of the selected function completes successfully.

  • If the selected function exits via an exception, it is propagated to the caller. Another function is then selected and executed.

The short example demonstrates the application of std::call_once and the std::once_flag. Both of them are declared in the header <mutex>.

Press + to interact
// callOnce.cpp
#include <iostream>
#include <thread>
#include <mutex>
std::once_flag onceFlag;
void do_once(){
std::call_once(onceFlag, [](){ std::cout << "Only once." << std::endl; });
}
int main(){
std::cout << std::endl;
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);
std::thread t4(do_once);
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << std::endl;
}

The program starts four threads (lines 17 - 20). Each of them invokes do_once. The expected result is that the string “only once” is displayed only once.

The famous singleton pattern guarantees that only one instance of an object will be created. This is a challenging task in multithreading environments. Thanks to std::call_once and std::once_flag the job is a piece of cake. Now the singleton is initialized in a thread-safe way.

Press + to interact
// singletonCallOnce.cpp
#include <iostream>
#include <mutex>
using namespace std;
class MySingleton{
private:
static once_flag initInstanceFlag;
static MySingleton* instance;
MySingleton() = default;
~MySingleton() = default;
public:
MySingleton(const MySingleton&) = delete;
MySingleton& operator=(const MySingleton&) = delete;
static MySingleton* getInstance(){
call_once(initInstanceFlag,MySingleton::initSingleton);
return instance;
}
static void initSingleton(){
instance= new MySingleton();
}
};
MySingleton* MySingleton::instance = nullptr;
once_flag MySingleton::initInstanceFlag;
int main(){
cout << endl;
cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << endl;
cout << "MySingleton::getInstance(): "<< MySingleton::getInstance() << endl;
cout << endl;
}

Let’s first review the static std::once_flag. It is declared in line 11 and initialized in line 31. The static method getInstance (lines 20 - 23) uses the flag initInstanceFlag to ensure that the static method initSingleton (line 25 - 27) is executed exactly once. The singleton is created in the body of the method.

i default and delete

You can request special methods from the compiler by using the keyword default. These methods are special because the compiler can create them for us. The result of annotating a method with delete is that the compiler generated methods will not be available and, therefore, cannot be called. If you try to use them, you’ll get a compile-time error. Here are the details for the keywords default and delete.

The MySingleton::getIstance() method displays the address of the singleton.

Static Variables with Block Scope

Static variables with block scope will be created exactly once and lazily. Lazily means that they are created just at the moment of the usage. This characteristic is the basis of the so-called Meyers Singleton, named after Scott Meyers. This is by far the most elegant implementation of the singleton pattern. With C++11, static variables with block scope have an additional guarantee; they will be initialized in a thread-safe way.

Here is the thread-safe Meyers Singleton pattern.

Press + to interact
// meyersSingleton.cpp
class MySingleton{
public:
static MySingleton& getInstance(){
static MySingleton instance;
return instance;
}
private:
MySingleton();
~MySingleton();
MySingleton(const MySingleton&)= delete;
MySingleton& operator=(const MySingleton&)= delete;
};
MySingleton::MySingleton()= default;
MySingleton::~MySingleton()= default;
int main(){
MySingleton::getInstance();
}

Know your Compiler support for static

If you use the Meyers Singleton pattern in a concurrent environment, be sure that your compiler implements static variables with the C++11 thread-safe semantic. It happens quite often that programmers rely on the C++11 semantic of static variables, but their compiler does not support it. The result may be that more than one instance of a singleton is created.

thread_local data has no sharing issues.

Get hands-on with 1400+ tech skills courses.