C++ Atomic Operations: Complete Guide with Examples and Scenarios
C++ Atomic Operations: Complete Guide with Examples and Scenarios
Atomic operations are the foundation of lock-free programming in C++. They provide thread-safe operations that are guaranteed to be indivisible.
Table of Contents
- What are Atomic Operations?
- std::atomic Types
- Atomic Operations
- Memory Ordering
- Common Patterns
- Example 1: Atomic Counter
- Example 2: Atomic Flag
- Example 3: Atomic Pointer
- Example 4: Atomic Operations with Memory Ordering
- Best Practices
What are Atomic Operations?
Atomic operations are indivisible - they complete entirely or not at all. Multiple threads can safely perform atomic operations on the same object without additional synchronization.
Key Properties
- Indivisible: Operation completes entirely or not at all
- Thread-safe: No data races
- Hardware support: Typically implemented with CPU instructions
- No locks: Avoid mutex overhead
When to Use
- Counters: Thread-safe increment/decrement
- Flags: Boolean flags shared between threads
- Pointers: Lock-free data structures
- Lock-free algorithms: Building blocks for complex structures
std::atomic Types
Basic Atomic Types
#include <atomic>
using namespace std;
// Integer types
atomic<int> int_atomic{0};
atomic<long> long_atomic{0L};
atomic<long long> llong_atomic{0LL};
// Pointer types
atomic<int*> ptr_atomic{nullptr};
// Boolean
atomic<bool> bool_atomic{false};
// Character types
atomic<char> char_atomic{'\0'};
Specialized Atomic Types
// Atomic flag (most efficient)
atomic_flag flag = ATOMIC_FLAG_INIT;
// Atomic shared pointer (C++20)
atomic<shared_ptr<int>> shared_ptr_atomic;
Custom Types
// Must be trivially copyable
struct Point {
int x, y;
};
atomic<Point> point_atomic{{0, 0}};
Atomic Operations
Load and Store
atomic<int> value{42};
// Load (read)
int read = value.load(); // Default: seq_cst
int read2 = value.load(memory_order_acquire);
// Store (write)
value.store(100); // Default: seq_cst
value.store(100, memory_order_release);
// Operator overloads
int val = value; // Equivalent to load()
value = 50; // Equivalent to store(50)
Exchange
atomic<int> value{10};
// Exchange (read and write atomically)
int old = value.exchange(20); // Set to 20, return old value
// value is now 20, old is 10
Compare-and-Swap
atomic<int> value{10};
// Weak version (may fail spuriously, faster)
int expected = 10;
bool success = value.compare_exchange_weak(expected, 20);
// If value == 10: set to 20, return true
// Otherwise: set expected to current value, return false
// Strong version (never fails spuriously, slower)
int expected2 = 10;
bool success2 = value.compare_exchange_strong(expected2, 20);
// Typical loop pattern
int expected = value.load();
do {
int desired = expected + 1;
} while (!value.compare_exchange_weak(expected, desired));
Read-Modify-Write Operations
atomic<int> counter{0};
// Fetch and add
int old = counter.fetch_add(5); // counter += 5, return old
counter += 5; // Same, but return new
// Fetch and subtract
int old2 = counter.fetch_sub(3);
// Fetch and bitwise operations
atomic<int> flags{0};
flags.fetch_and(0xFF); // flags &= 0xFF
flags.fetch_or(0x01); // flags |= 0x01
flags.fetch_xor(0x10); // flags ^= 0x10
// Pre/post increment/decrement
++counter; // Pre-increment
counter++; // Post-increment
--counter; // Pre-decrement
counter--; // Post-decrement
Memory Ordering
Memory ordering controls synchronization between atomic operations and regular memory operations.
Memory Order Options
// Sequential consistency (default, strongest)
atomic<int> x{0};
x.store(1, memory_order_seq_cst);
int val = x.load(memory_order_seq_cst);
// Acquire (for loads) - can't reorder before this
int val2 = x.load(memory_order_acquire);
// Release (for stores) - can't reorder after this
x.store(1, memory_order_release);
// Acquire-Release (for RMW) - both acquire and release
x.fetch_add(1, memory_order_acq_rel);
// Relaxed (weakest) - no ordering guarantees
x.store(1, memory_order_relaxed);
int val3 = x.load(memory_order_relaxed);
Memory Order Semantics
memory_order_relaxed: No synchronization, just atomicitymemory_order_acquire: Loads can’t be reordered before thismemory_order_release: Stores can’t be reordered after thismemory_order_acq_rel: Both acquire and releasememory_order_seq_cst: Sequential consistency (default, strongest)
Release-Acquire Pattern
atomic<bool> ready{false};
int data = 0;
// Thread 1: Producer
void producer() {
data = 42; // Regular write
ready.store(true, memory_order_release); // Release: all writes before this are visible
}
// Thread 2: Consumer
void consumer() {
while (!ready.load(memory_order_acquire)) { // Acquire: see all writes before release
// Wait
}
// data is guaranteed to be 42 here
cout << data << endl;
}
Sequential Consistency
atomic<int> x{0}, y{0};
// Thread 1
void thread1() {
x.store(1, memory_order_seq_cst); // Sequentially consistent
int r1 = y.load(memory_order_seq_cst);
}
// Thread 2
void thread2() {
y.store(1, memory_order_seq_cst);
int r2 = x.load(memory_order_seq_cst);
}
// With seq_cst, it's impossible for both r1 and r2 to be 0
Common Patterns
Pattern 1: Atomic Counter
class AtomicCounter {
private:
atomic<int> count_{0};
public:
void increment() {
count_.fetch_add(1, memory_order_relaxed);
}
void decrement() {
count_.fetch_sub(1, memory_order_relaxed);
}
int get() const {
return count_.load(memory_order_acquire);
}
int getAndReset() {
return count_.exchange(0, memory_order_acq_rel);
}
};
Pattern 2: Atomic Flag
class AtomicFlag {
private:
atomic<bool> flag_{false};
public:
void set() {
flag_.store(true, memory_order_release);
}
bool test() {
return flag_.load(memory_order_acquire);
}
bool testAndSet() {
return flag_.exchange(true, memory_order_acq_rel);
}
void clear() {
flag_.store(false, memory_order_release);
}
};
Pattern 3: Spin Lock
class SpinLock {
private:
atomic_flag locked_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (locked_.test_and_set(memory_order_acquire)) {
// Spin until lock is acquired
while (locked_.test(memory_order_relaxed)) {
// CPU pause hint
this_thread::yield();
}
}
}
void unlock() {
locked_.clear(memory_order_release);
}
};
Example 1: Atomic Counter
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
using namespace std;
void atomicCounterExample() {
atomic<int> counter{0};
const int NUM_THREADS = 10;
const int INCREMENTS_PER_THREAD = 1000;
vector<thread> threads;
for (int i = 0; i < NUM_THREADS; ++i) {
threads.emplace_back([&counter]() {
for (int j = 0; j < INCREMENTS_PER_THREAD; ++j) {
counter.fetch_add(1, memory_order_relaxed);
}
});
}
for (auto& t : threads) {
t.join();
}
cout << "Final counter value: " << counter.load() << endl;
cout << "Expected: " << NUM_THREADS * INCREMENTS_PER_THREAD << endl;
}
Example 2: Atomic Flag
void atomicFlagExample() {
atomic<bool> ready{false};
string message;
thread producer([&]() {
this_thread::sleep_for(chrono::milliseconds(100));
message = "Hello from producer";
ready.store(true, memory_order_release);
});
thread consumer([&]() {
while (!ready.load(memory_order_acquire)) {
this_thread::yield();
}
cout << "Message: " << message << endl;
});
producer.join();
consumer.join();
}
Example 3: Atomic Pointer
void atomicPointerExample() {
atomic<int*> ptr{nullptr};
int* data = new int(42);
thread writer([&]() {
this_thread::sleep_for(chrono::milliseconds(50));
ptr.store(data, memory_order_release);
});
thread reader([&]() {
int* p;
while (!(p = ptr.load(memory_order_acquire))) {
this_thread::yield();
}
cout << "Read value: " << *p << endl;
});
writer.join();
reader.join();
delete data;
}
Example 4: Atomic Operations with Memory Ordering
void memoryOrderingExample() {
atomic<int> data{0};
atomic<bool> ready{false};
// Thread 1: Producer
thread producer([&]() {
data.store(100, memory_order_relaxed);
data.store(200, memory_order_relaxed);
ready.store(true, memory_order_release); // Release: all previous stores visible
});
// Thread 2: Consumer
thread consumer([&]() {
while (!ready.load(memory_order_acquire)) { // Acquire: see all stores before release
this_thread::yield();
}
// data is guaranteed to be 200 (or later value)
cout << "Data: " << data.load(memory_order_relaxed) << endl;
});
producer.join();
consumer.join();
}
Best Practices
1. Use Appropriate Memory Ordering
// GOOD: Correct ordering for synchronization
atomic<bool> flag{false};
int data = 0;
void set() {
data = 42;
flag.store(true, memory_order_release);
}
void get() {
if (flag.load(memory_order_acquire)) {
// data is guaranteed to be 42
}
}
// BAD: Relaxed ordering (no synchronization)
void bad_set() {
data = 42;
flag.store(true, memory_order_relaxed); // No ordering!
}
2. Prefer Weak CAS in Loops
// GOOD: Weak CAS in loop (may be faster)
atomic<int> value{10};
int expected = value.load();
do {
int desired = expected + 1;
} while (!value.compare_exchange_weak(expected, desired));
// Use strong CAS when not in loop
bool success = value.compare_exchange_strong(expected, desired);
3. Avoid False Sharing
// BAD: False sharing (same cache line)
struct {
atomic<int> counter1;
atomic<int> counter2; // Same cache line!
} counters;
// GOOD: Separate cache lines
alignas(64) atomic<int> counter1;
alignas(64) atomic<int> counter2;
4. Use atomic_flag for Flags
// GOOD: Most efficient for boolean flags
atomic_flag flag = ATOMIC_FLAG_INIT;
flag.test_and_set();
flag.clear();
// Also OK, but less efficient
atomic<bool> flag2{false};
Common Mistakes
- Wrong memory ordering: Using relaxed when acquire/release needed
- Mixing atomic and non-atomic: Accessing same data both ways
- False sharing: Multiple atomics on same cache line
- ABA problem: Not handling in CAS loops
- Infinite loops: CAS loops without proper exit
Summary
Atomic operations provide thread-safe, lock-free operations:
- Indivisible operations: Guaranteed atomicity
- Memory ordering: Control synchronization
- Lock-free: No mutex overhead
- Performance: Better scalability
Key takeaways:
- Use
std::atomicfor shared data - Understand memory ordering semantics
- Use appropriate ordering for your needs
- Avoid false sharing
- Prefer weak CAS in loops
By mastering atomic operations, you can build efficient, lock-free concurrent systems in C++.