Using atomics the normal way – high level interface
Instead of using explicit lock mechanisms you can define any variable of integral type (bool, integer, pointer type) as “atomic”. Then the locking will be done automatically each time you access the variable.
Basic Example
The following sample illustrates safe data access to the variables of the preceding section:
#include <atomic>
// Global data definition with initial values
int g_someCounter = 0;
double g_someValue = 0.0;
// Define and initialize the atomic object
// to guarantee proper locking
std::atomic<bool> g_ready(false);
Some worker thread changes data and signals the end of his work by setting the ready flag.
// Thread changing global data
g_someCounter = 47;
g_someValue = 3.14;
g_ready.store(true);
// "release operation"
// ensures that all memory operations are executed
// and are visible to other threads, i.e.
// all variables have already changed their values
Some other thread wants to process the changed data:
// Thread using global data
if (g_ready.load())
// "aquire operation" for affected memory
{
myCounter += g_someCounter;
myValue = g_someValue * 2;
}
Most used features
Method | Description |
---|---|
a.store(val) | safely sets the given value |
a.load() | safely returns the current value |
a+=val, a-=val, a++, ++a, a&=val, a|=val | provides the usual functionality, implicitly calls load() or store() |
Limitations
- std::atomic is mainly suitable for protecting isolated integral values, e.g. some counter which may be read and incremented by several threads.
- if the internal state of an object consists of two or more attributes which may repeatedly change their values you have to use other synchronisation mechanisms, e.g. std::mutex.
Using atomics the “relaxed” way – low-level interface only for experts
Reading and writing shared data when running several threads on a multicore system may lead to problems with the execution order of C++ statements. For example a reader thread may observe value changes in a different order than the writer thread has executed these changes.
If you use std::atomic the way described in the previous section, then load() and store() are used with default argument “std::memory_order_seq_cst” (“sequential consistent memory order”). With this settings you can assume that all threads see the same order of changes.
To allow a more optimized code generation, it is possible to weaken the requirements for the memory model. For writing data the “std::memory_order_release” and for reading data “std::memory_order_aquire” would be sufficient.
If there are no dependencies to other data, you may even use “std::memory_order_relaxed”:
g_ready.store(true,std::memory_order_relaxed);
if (g_ready.load(std::memory_order_relaxed))
{
...
}
std::memory_order – complete reference at CppReference.com
Recommendation
If you have no need for optimization and if you are not yet a multithreading expert always use the high-level interface for atomics.