The previous section introduced the simplest atomic type in C++11, std::atomic_flag, but it's too limited with only test_and_set and clear APIs. It doesn't meet other requirements like store, load, exchange, and compare_exchange. This article will explore the more comprehensive std::atomic class.
Basic std::atomic Types Explained
Introduction to std::atomic
std::atomic is a template class that encapsulates a value of type T within an atomic object.
template <class T> struct atomic;
The main characteristic of atomic objects is that accessing them from different threads doesn't lead to data races. Therefore, accessing an atomic object from multiple threads is well-defined behavior, whereas concurrent access to non-atomic objects (without synchronization) typically results in undefined behavior.
The C++11 standard defines the basic std::atomic template as follows:
template < class T > struct atomic {
bool is_lock_free() const volatile;
bool is_lock_free() const;
void store(T, memory_order = memory_order_seq_cst) volatile;
void store(T, memory_order = memory_order_seq_cst);
T load(memory_order = memory_order_seq_cst) const volatile;
T load(memory_order = memory_order_seq_cst) const;
operator T() const volatile;
operator T() const;
T exchange(T, memory_order = memory_order_seq_cst) volatile;
T exchange(T, memory_order = memory_order_seq_cst);
bool compare_exchange_weak(T &, T, memory_order, memory_order) volatile;
bool compare_exchange_weak(T &, T, memory_order, memory_order);
bool compare_exchange_strong(T &, T, memory_order, memory_order) volatile;
bool compare_exchange_strong(T &, T, memory_order, memory_order);
bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_weak(T &, T, memory_order = memory_order_seq_cst);
bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_strong(T &, T, memory_order = memory_order_seq_cst);
atomic() = default;
constexpr atomic(T);
atomic(const atomic &) = delete;
atomic & operator=(const atomic &) = delete;
atomic & operator=(const atomic &) volatile = delete;
T operator=(T) volatile;
T operator=(T);
};
Additionally, the C++11 standard library provides specialized implementations for integral and pointer types. The specialization for integral types (which include char, signed char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long, char16_t, char32_t, wchar_t) is defined as follows:
template <> struct atomic<integral> {
bool is_lock_free() const volatile;
bool is_lock_free() const;
void store(integral, memory_order = memory_order_seq_cst) volatile;
void store(integral, memory_order = memory_order_seq_cst);
integral load(memory_order = memory_order_seq_cst) const volatile;
integral load(memory_order = memory_order_seq_cst) const;
operator integral() const volatile;
operator integral() const;
integral exchange(integral, memory_order = memory_order_seq_cst) volatile;
integral exchange(integral, memory_order = memory_order_seq_cst);
bool compare_exchange_weak(integral&, integral, memory_order, memory_order) volatile;
bool compare_exchange_weak(integral&, integral, memory_order, memory_order);
bool compare_exchange_strong(integral&, integral, memory_order, memory_order) volatile;
bool compare_exchange_strong(integral&, integral, memory_order, memory_order);
bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_weak(integral&, integral, memory_order = memory_order_seq_cst);
bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_strong(integral&, integral, memory_order = memory_order_seq_cst);
integral fetch_add(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_add(integral, memory_order = memory_order_seq_cst);
integral fetch_sub(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_sub(integral, memory_order = memory_order_seq_cst);
integral fetch_and(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_and(integral, memory_order = memory_order_seq_cst);
integral fetch_or(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_or(integral, memory_order = memory_order_seq_cst);
integral fetch_xor(integral, memory_order = memory_order_seq_cst) volatile;
integral fetch_xor(integral, memory_order = memory_order_seq_cst);
atomic() = default;
constexpr atomic(integral);
atomic(const atomic&) = delete;
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete;
integral operator=(integral) volatile;
integral operator=(integral);
integral operator++(int) volatile;
integral operator++(int);
integral operator--(int) volatile;
integral operator--(int);
integral operator++() volatile;
integral operator++();
integral operator--() volatile;
integral operator--();
integral operator+=(integral) volatile;
integral operator+=(integral);
integral operator-=(integral) volatile;
integral operator-=(integral);
integral operator&=(integral) volatile;
integral operator&=(integral);
integral operator|=(integral) volatile;
integral operator|=(integral);
integral operator^=(integral) volatile;
integral operator^=(integral);
};
The specialization for pointers is defined as:
template <class T> struct atomic<T*> {
bool is_lock_free() const volatile;
bool is_lock_free() const;
void store(T*, memory_order = memory_order_seq_cst) volatile;
void store(T*, memory_order = memory_order_seq_cst);
T* load(memory_order = memory_order_seq_cst) const volatile;
T* load(memory_order = memory_order_seq_cst) const;
operator T*() const volatile;
operator T*() const;
T* exchange(T*, memory_order = memory_order_seq_cst) volatile;
T* exchange(T*, memory_order = memory_order_seq_cst);
bool compare_exchange_weak(T*&, T*, memory_order, memory_order) volatile;
bool compare_exchange_weak(T*&, T*, memory_order, memory_order);
bool compare_exchange_strong(T*&, T*, memory_order, memory_order) volatile;
bool compare_exchange_strong(T*&, T*, memory_order, memory_order);
bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_weak(T*&, T*, memory_order = memory_order_seq_cst);
bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst) volatile;
bool compare_exchange_strong(T*&, T*, memory_order = memory_order_seq_cst);
T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
T* fetch_add(ptrdiff_t, memory_order = memory_order_seq_cst);
T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst) volatile;
T* fetch_sub(ptrdiff_t, memory_order = memory_order_seq_cst);
atomic() = default;
constexpr atomic(T*);
atomic(const atomic&) = delete;
atomic& operator=(const atomic&) = delete;
atomic& operator=(const atomic&) volatile = delete;
T* operator=(T*) volatile;
T* operator=(T*);
T* operator++(int) volatile;
T* operator++(int);
T* operator--(int) volatile;
T* operator--(int);
T* operator++() volatile;
T* operator++();
T* operator--() volatile;
T* operator--();
T* operator+=(ptrdiff_t) volatile;
T* operator+=(ptrdiff_t);
T* operator-=(ptrdiff_t) volatile;
T* operator-=(ptrdiff_t);
};
std::atomic Member Functions
Now that we have a basic understanding of std::atomic, let's examine its member functions.
std::atomic Constructors
The constructors for std::atomic are as follows:
| default (1) | atomic() noexcept = default; |
|---|---|
| initialization (2) | constexpr atomic (T val) noexcept; |
| copy \[deleted\] (3) | atomic (const atomic&) = delete; |
- Default constructor: Creates an uninitialized
std::atomicobject, which can be initialized later with theatomic_initfunction. - Initialization constructor: Initializes a
std::atomicobject with a value of type T. - Copy constructor is disabled to prevent unsafe copying.
Here's an example:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
// Initialize an atomic<bool> with false
std::atomic<bool> ready(false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;
void count_to_1m(int id)
{
while (!ready) {
std::this_thread::yield();
}
for (volatile int i = 0; i < 1000000; ++i) {}
if (!winner.test_and_set()) {
std::cout << "thread #" << id << " won!\n";
}
}
int main()
{
std::vector<std::thread> threads;
std::cout << "spawning 10 threads that count to 1 million...\n";
for (int i = 1; i <= 10; ++i) {
threads.push_back(std::thread(count_to_1m, i));
}
ready = true;
for (auto& th : threads) {
th.join();
}
return 0;
}
std::atomic::operator=() Function
The assignment operators for std::atomic are defined as:
| set value (1) | T operator= (T val) noexcept; T operator= (T val) volatile noexcept; |
|---|---|
| copy \[deleted\] (2) | atomic& operator= (const atomic&) = delete; atomic& operator= (const atomic&) volatile = delete; |
Regular copy assignment is disabled. However, a value of type T can be assigned to the corresponding atomic type variable (an implicit conversion), which is atomic with a default memory order of std::memory_order_seq_cst. To specify a different memory order, use std::atomic::store().
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter = 0;
void set_counter(int x)
{
counter = x; // Calls std::atomic::operator=()
}
void print_counter()
{
while (counter == 0) {
std::this_thread::yield();
}
std::cout << "counter: " << counter << '\n';
}
int main()
{
std::thread first(print_counter);
std::thread second(set_counter, 10);
first.join();
second.join();
return 0;
}
Basic std::atomic Type Operations
This section covers the operations available for basic std::atomic types. As mentioned earlier, std::atomic is a template class, and besides the basic type, there are specializations for integral and pointer types that support additional operations like fetch_add, fetch_sub, fetch_and, etc. This section focuses on operations available for basic std::atomic types:
is_lock_free
bool is_lock_free() const volatile noexcept;
bool is_lock_free() const noexcept;
Checks whether the std::atomic object is lock-free. If an object is lock-free, multiple threads can access it without causing thread blocking (possibly implemented using transactional memory methods).
store
void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) noexcept;
Modifies the encapsulated value. The std::atomic::store function copies the parameter val of type T to the value encapsulated by the atomic object. The sync parameter specifies the memory order, with possible values:
| Memory Order Value | Memory Order Type |
|---|---|
| memory\_order\_relaxed | Relaxed |
| memory\_order\_release | Release |
| memory\_order\_seq\_cst | Sequentially consistent |
Here's an example:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> value(0);
void set_value(int x)
{
value.store(x, std::memory_order_relaxed);
}
void print_value()
{
int x;
do {
x = value.load(std::memory_order_relaxed);
} while (x == 0);
std::cout << "value: " << x << '\n';
}
int main()
{
std::thread first(print_value);
std::thread second(set_value, 10);
first.join();
second.join();
return 0;
}
load
T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;
T load (memory_order sync = memory_order_seq_cst) const noexcept;
Reads the encapsulated value. The sync parameter sets the memory order, with possible values:
| Memory Order Value | Memory Order Type |
|---|---|
| memory\_order\_relaxed | Relaxed |
| memory\_order\_consume | Consume |
| memory\_order\_acquire | Acquire |
| memory\_order\_seq\_cst | Sequentially consistent |
Example:
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> data(0);
void set_data(int x)
{
data.store(x, std::memory_order_relaxed);
}
void print_data()
{
int x;
do {
x = data.load(std::memory_order_relaxed);
} while (x == 0);
std::cout << "data: " << x << '\n';
}
int main()
{
std::thread first(print_data);
std::thread second(set_data, 10);
first.join();
second.join();
return 0;
}
operator T
operator T() const volatile noexcept;
operator T() const noexcept;
Similar to load, this reads the encapsulated value. operator T() is a type-cast operation with a default memory order of std::memory_order_seq_cst. If you need to specify a different memory order, use the load() function instead.
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> source = 0;
std::atomic<int> destination = 0;
void set_source(int x)
{
source = x;
}
void copy_source_to_destination()
{
while (source == 0) {
std::this_thread::yield();
}
destination = static_cast<int>(source);
}
void print_destination()
{
while (destination == 0) {
std::this_thread::yield();
}
std::cout << "destination: " << destination << '\n';
}
int main()
{
std::thread first(print_destination);
std::thread second(set_source, 10);
std::thread third(copy_source_to_destination);
first.join();
second.join();
third.join();
return 0;
}
exchange
T exchange (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
T exchange (T val, memory_order sync = memory_order_seq_cst) noexcept;
Reads and modifies the encapsulated value. exchange replaces the value encapsulated by the atomic object with the value specified by val and returns the previous value. The entire operation is atomic (also called a read-modify-write operation). The sync parameter specifies the memory order, with possible values:
| Memory Order Value | Memory Order Type |
|---|---|
| memory\_order\_relaxed | Relaxed |
| memory\_order\_consume | Consume |
| memory\_order\_acquire | Acquire |
| memory\_order\_release | Release |
| memory\_order\_acq\_rel | Acquire/Release |
| memory\_order\_seq\_cst | Sequentially consistent |
Example where multiple threads count to 1M, and the first to finish prints its ID:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<bool> start_flag(false);
std::atomic<bool> finish_flag(false);
void process_count(int id)
{
while (!start_flag) {}
for (int i = 0; i < 1000000; ++i) {}
if (!finish_flag.exchange(true)) {
std::cout << "thread #" << id << " won!\n";
}
}
int main()
{
std::vector<std::thread> threads;
std::cout << "spawning 10 threads that count to 1 million...\n";
for (int i = 1; i <= 10; ++i) {
threads.push_back(std::thread(process_count, i));
}
start_flag = true;
for (auto& th : threads) {
th.join();
}
return 0;
}
compare_exchange_weak
Atomic addition and subtraction are typically implemented using CAS (Compare and Swap), which is platform-dependent. The basic form of CAS is: CAS(addr, old, new), which replaces the value at addr with new if it equals old.
bool compare_exchange_weak (T& expected, T val,
memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,
memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_weak (T& expected, T val,
memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,
memory_order success, memory_order failure) noexcept;
Compares and exchanges the encapsulated value with the value specified by expected. If they are equal, it replaces the atomic object's value with val. If they are not equal, it replaces expected with the atomic object's current value. After calling this function, if the atomic object's value was not equal to expected, expected will contain the atomic object's previous value.
The function typically reads the encapsulated value and, if the comparison is true (the atomic object's value equals expected), replaces the atomic object's value. The entire operation is atomic, preventing other threads from reading or modifying the atomic object during the operation.
In the second form, the memory order depends on the comparison result. If the comparison is true, it uses the memory order specified by success; otherwise, it uses the memory order specified by failure.
Note that this function directly compares the physical content of the atomic object's encapsulated value with expected. In some cases, objects might be equal when compared with operator==() but fail with compare_exchange_weak due to differences in physical representation (like true and 2 or 3, which are logically equivalent but physically different).
Unlike compare_exchange_strong, the weak version of compare-and-exchange may spuriously return false even when the values are physically equal. However, this is acceptable in some algorithms that use loops, and compare_exchange_weak may perform better on some platforms. If a spurious failure occurs, the function returns false, and expected remains unchanged.
For algorithms that don't require loops, compare_exchange_strong is generally preferred. The memory order is specified by the sync parameter, with possible values:
| Memory Order Value | Memory Order Type |
|---|---|
| memory\_order\_relaxed | Relaxed |
| memory\_order\_consume | Consume |
| memory\_order\_acquire | Acquire |
| memory\_order\_release | Release |
| memory\_order\_acq\_rel | Acquire/Release |
| memory\_order\_seq\_cst | Sequentially consistent |
Example:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
struct ListNode { int data; ListNode* next; };
std::atomic<ListNode*> list_head(nullptr);
void add_to_list(int val)
{
ListNode* new_node = new ListNode{val, list_head};
while (!list_head.compare_exchange_weak(new_node->next, new_node)) {
// new_node->next is updated if another thread added a node
}
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(add_to_list, i));
}
for (auto& th : threads) {
th.join();
}
for (ListNode* current = list_head; current != nullptr; current = current->next) {
std::cout << ' ' << current->data;
}
std::cout << '\n';
ListNode* current;
while (current = list_head) {
list_head = current->next;
delete current;
}
return 0;
}
Possible output:
9 8 7 6 5 4 3 2 1 0
compare_exchange_strong
bool compare_exchange_strong (T& expected, T val,
memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_strong (T& expected, T val,
memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_strong (T& expected, T val,
memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_strong (T& expected, T val,
memory_order success, memory_order failure) noexcept;
Compares and exchanges the encapsulated value with the value specified by expected. If they are equal, it replaces the atomic object's value with val. If they are not equal, it replaces expected with the atomic object's current value. After calling this function, if the atomic object's value was not equal to expected, expected will contain the atomic object's previous value.
The function typically reads the encapsulated value and, if the comparison is true (the atomic object's value equals expected), replaces the atomic object's value. The entire operation is atomic, preventing other threads from reading or modifying the atomic object during the operation.
In the second form, the memory order depends on the comparison result. If the comparison is true, it uses the memory order specified by success; otherwise, it uses the memory order specified by failure.
Note that this function directly compares the physical content of the atomic object's encapsulated value with expected. In some cases, objects might be equal when compared with operator==() but fail with compare_exchange_strong due to differences in physical representation.
Unlike compare_exchange_weak, the strong version of compare-and-exchange does not spuriously return false when the values are physically equal. However, on some platforms, compare_exchange_weak may perform better in algorithms that use loops for checking.
For algorithms that don't require loops, compare_exchange_strong is generally preferred. The memory order is specified by the sync parameter, with possible values:
| Memory Order Value | Memory Order Type |
|---|---|
| memory\_order\_relaxed | Relaxed |
| memory\_order\_consume | Consume |
| memory\_order\_acquire | Acquire |
| memory\_order\_release | Release |
| memory\_order\_acq\_rel | Acquire/Release |
| memory\_order\_seq\_cst | Sequentially consistent |
Example:
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
struct Node { int value; Node* next; };
std::atomic<Node*> head(nullptr);
void insert_node(int val)
{
Node* new_node = new Node{val, head};
while (!(head.compare_exchange_strong(new_node->next, new_node))) {
// new_node->next is updated if another thread added a node
}
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.push_back(std::thread(insert_node, i));
}
for (auto& th : threads) {
th.join();
}
for (Node* iter = head; iter != nullptr; iter = iter->next) {
std::cout << ' ' << iter->value;
}
std::cout << '\n';
Node* iter;
while (iter = head) {
head = iter->next;
delete iter;
}
return 0;
}
This article has covered the basic std::atomic types in detail. The next article will explore the enhancements made in the C++11 standard library for specialized versions of std::atomic for integral and pointer types.