std::atomic
is for data accessed from multiple threads without using mutexes (concurrent usage); volatile
is for memory where reads and writes should not be optimised away (special memory).
std::atomic
template
Instantiations of std::atomic
template offer operations that are guaranteed to be seen as atomic by other threads, as if they were inside a mutex-protected critical section, generally with the support of special machine instructions that are more efficient than the case of mutex. For example:
|
|
During execution of these statements, other threads reading ai
may see only values of 0, 10, or 11 (assuming, of course, this is the only thread modifying ai
). Two things worth noting here:
- For
std::cout << ai;
, only the read ofai
is atomic, so it’s possible that between the timeai
’s value is rad andoperator<<
is invoked to write it to standard output, another thread may modifyai
’s value. - The increment and decrement of
ai
are read-modify-write (RMW) operations, and they execute atomatically as well, which is one of the nicest characteristics of thestd::atomic
types that they guarantee all member functions onstd::atomic
types will be seen by other threads as atomic. - The use of
std::atomic
imposes restrictions that no code precedes a write of astd::atomic
variable may take place afterwards. No reorder tricks for compiler/hardwaes for speed-up optimization purpose.
In contrast, volatile
offers no guarantee of operation atomicity and suffer insufficient restrictions on code reordering - basically not useful in multithreaded context. Say if we have a counter defined as volatile int vc(0)
, and there are two threads increment the volatile
counter simultaneously, then the ending value of vc
need not be 2
- the RMW operation in each of two threads may take place in any order, involving in a data race, which leading to undefined behavior according to Standard’s decree.
The place in which volatile
shines is in the context where redundant loads and dead stores should not be optimized away, that is, we need special memory to perform such kinds of redundent reads and superfluous writes:
|
|
The most common kind of special memory is memory used for memory-mapped I/O, which is used for communication with peripherals, e.g., external sensors or displays, printers, network ports, etc. rather than reading or writing normal memory (i.e., RAM). volatile
is the way to tell compilers that we’re dealing with special memory.
Because std::atomic
and volatile
serve different purposes, they can be used together:
|
|
This could be useful if vai
corresponded to a memory-mapped I/O location that was concurrently accessed by multiple threads.