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.
// 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 workstd::this_thread::sleep_for(std::chrono::milliseconds(200));// end workstd::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.
// 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 workstd::this_thread::sleep_for(std::chrono::milliseconds(200));// end workcoutMutex.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-safeThe 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
, andstd::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++17With C++17 we get a new mutex:
std::shared_mutex
.std::shared_mutex
is similar tostd::shared_timed_mutex
. According tostd::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.
std::mutex m;m.lock();sharedVariable = getVar();m.unlock();
Here are the issues:
-
If the function
getVar()
throws an exception, the mutexm
will not be released. -
Never ever call an unknown function while holding a lock. If the function
getVar
tries to lock the mutexm
, the program has undefined behavior becausem
is not a recursive mutex. Most of the time, the undefined behavior will result in a deadlock. -
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.
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.
// 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 ba.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.
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.:
{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++17With C++17 we get a
std::scoped_lock
. It’s very similar tostd::unique_lock
, butstd::scoped_lock
can lock an arbitrary number of mutexes atomically. You have to keep two facts in mind.
If one of the current threads already owns the corresponding mutex and the mutex is not recursive, the behaviour is undefined.
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?
// 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 ba.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.
// 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.
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:
// 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 astd::lock_guard
instead of thestd::lock
call. That’s all. Here is the modified functiondeadlock
.// 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.
// 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.
// 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
:
// 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 flagstd::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.
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.
// 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>
.
// 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.
// 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
anddelete
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 withdelete
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.
// meyersSingleton.cppclass 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.