1012 Chapter 18: Concurrency
18.7 Atomics
In the first example for condition variables (see Section 18.6.1, page 1003), we used a Boolean
value readyFlag to let one thread signal that something is prepared or provided for another thread.
Now, you might wonder why we still need a mutex here. If we have a Boolean value, why can’t
we concurrently let one thread change the value while another thread checks it? The moment the
providing thread sets the Boolean to true, the observing thread should be able to see that and
perform the consequential processing.
As introduced in Section 18.4, page 982, we have two problems here:
1. In general, reading and writing even for fundamental data types is not atomic. Thus, you might
read a half-written Boolean, which according to the standard results in undefined behavior.
2. The generated code might change the order of operations, so the providing thread might set the
ready flag before the data is provided, and the consuming thread might process the data before
evaluating the ready flag.
With a mutex, both problems are solved, but a mutex might be a relatively expensive operation in
both necessary resources and latency of the exclusive access. So, instead of using mutexes and lock,
it might be worth using atomics instead.
In this section, I first introduce the high-level interface of atomics, which provides atomic operations
using the default guarantee regarding the order of memory access. This default guarantee
provides sequential consistency, which means that in a thread, atomic operations are guaranteed to
happen in the order as programmed. Thus, problems of reordered statements as introduced in Section
18.4.3, page 986, do not apply. At the end of this section, I present the low-level interface of
atomics: operations with relaxed order guarantees.
Note that the C++ standard library does not distinguish between a high-level and a low-level
atomics interface. The term low-level was introduced by Hans Boehm, one of the authors of the library.
Sometimes, it is also called the weak, or relaxed, atomic interface, and the high-level interface
is sometimes also known as the normal, or strong, atomic interface.
18.7.1 Example of Using Atomics
Let’s transfer the example from Section 18.6.1, page 1003, into a program using atomics:
#include // for atomic types
...
std::atomic readyFlag(false);
void thread1()
{
// do something thread2 needs as preparation
...
readyFlag.store(true);
}
www.it-ebooks.info
18.7 Atomics 1013
void thread2()
{
// wait until readyFlag is true (thread1 is done)
while (!readyFlag.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
// do whatever shall happen after thread1 has prepared things
...
}
First, we include the header file , where atomics are declared:
#include
Then, we declare an atomic object, using the std::atomic class template:
std::atomic readyFlag(false);
In principle, you can use any trivial, integral, or pointer type as template parameter.
Note that you always should initialize atomic objects because the default constructor does not
fully initialize it (it’s not that the initial value is undefined, it is that the lock is uninitialized).26
For static-duration atomic objects, you should use a constant to initialize them. If only the default
constructor is used, the only operation allowed next is to call a global atomic_init() operation as
follows:
std::atomic readyFlag;
...
std::atomic_init(&readyFlag,false);
This way of initialization is provided to be able to write code that also compiles in C (see Section
18.7.3, page 1019).
The two most important statements to deal with atomics are store() and load():
• store() assigns a new value.
• load() yields the current value.
The important point is that these operations are guaranteed to be atomic, so we don’t need a mutex
to set the ready flag, as we had to without atomics. Thus, in the first thread, instead of
{
std::lock_guard lg(readyMutex);
readyFlag = true;
} // release lock
we simply can program:
readyFlag.store(true);
26 Thanks to Lawrence Crowl for pointing this out.
www.it-ebooks.info
1014 Chapter 18: Concurrency
In the second thread, instead of
{
std::unique_lock l(readyFlagMutex);
while (!readyFlag) {
l.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
l.lock();
}
} // release lock
we have to implement only the following:
while (!readyFlag.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
However, when using condition variables, we still need the mutex for consuming the condition
variable:
// wait until thread1 is ready (readyFlag is true)
{
std::unique_lock l(readyMutex);
readyCondVar.wait(l, []{ return readyFlag.load(); });
} // release lock
For atomic types, you can still use the “useful,” “ordinary” operations, such as assignments, automatic
conversions to integral types, increments, decrements, and so on:
std::atomic ab(false);
ab = true;
if (ab) {
...
}
std::atomic ai(0);
int x = ai;
ai = 10;
ai++;
ai-=17;
Note, however, that to provide atomicity, some usual behavior might be slightly different. For example,
the assignment operator yields the assigned value instead of a reference to the atomic the value
was assigned to. See Section 18.7.2, page 1016, for details.
Let’s look at a complete example using atomics:
// concurrency/atomics1.cpp
#include // for atomics
#include // for async() and futures
www.it-ebooks.info
18.7 Atomics 1015
#include // for this_thread
#include // for durations
#include
long data;
std::atomic readyFlag(false);
void provider ()
{
// after reading a character
std::cout