Lesson 27 • Advanced
Atomic Operations & Low-Level Synchronization
Use std::atomic for lock-free counters, compare-and-swap, spinlocks, and memory ordering for maximum concurrent performance.
What You'll Learn
- ✓ atomic load, store, fetch_add, exchange
- ✓ Compare-and-swap (CAS) loops
- ✓ Spinlocks with atomic_flag
- ✓ Memory ordering basics
Why Atomics?
Mutexes are safe but heavy — they involve OS kernel calls and thread scheduling. Atomic operations are hardware-level instructions that complete in a single CPU cycle, with no locks and no waiting. They're the fastest way to synchronise threads.
Think of a mutex like taking a number at a deli counter — you wait your turn. An atomic operation is like an instant, indivisible action — it either happens completely or not at all, with no possibility of interference.
Atomic Basics
Compare atomic vs non-atomic counters and explore atomic operations
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
// atomic — lock-free thread-safe operations
atomic<int> atomicCounter(0);
int unsafeCounter = 0;
void incrementAtomic(int n) {
for (int i = 0; i < n; i++) {
atomicCounter++; // Thread-safe, no mutex needed
}
}
void incrementUnsafe(int n) {
for (int i = 0; i < n; i++) {
unsafeCounter++; // Data race!
}
}
int main() {
const int N = 100000;
const int THREADS
...Compare-and-Exchange
Build a lock-free stack using CAS loops
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
// Lock-free stack using compare_exchange
template <typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(nullptr) {}
};
atomic<Node*> head{nullptr};
atomic<int> size_{0};
public:
void push(const T& value) {
Node* newNode = new Node(value);
newNode->next = head.load();
...Spinlocks & Memory Ordering
Implement spinlocks and atomic signalling between threads
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
using namespace std;
// Spinlock using atomic_flag
class SpinLock {
atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock() {
// Spin until we acquire the lock
while (flag.test_and_set(memory_order_acquire)) {
// Busy waiting — burns CPU but very fast for short locks
}
}
void unlock() {
flag.clear(memory_order_release);
}
};
// atomic<bool> for signall
...Common Mistakes
⚠️ Atomics aren't magic: atomic<int> a, b; — reading a and b separately is still two operations. Use a mutex for compound atomicity.
⚠️ ABA problem: CAS can succeed even if the value changed and changed back. Use version counters or hazard pointers for complex data structures.
⚠️ Relaxed ordering bugs: memory_order_relaxed gives no ordering guarantees between threads. Use acquire/release unless you deeply understand the implications.
Pro Tips
💡 Default to seq_cst: memory_order_seq_cst (the default) is the safest ordering. Only relax it when profiling shows it's a bottleneck.
💡 Prefer mutex for complex logic: Atomics are for simple counters and flags. For complex shared state, mutexes are safer and clearer.
💡 Check is_lock_free: Not all atomic types use hardware atomics. Check is_lock_free() — if false, there's a hidden mutex.
📋 Quick Reference
| Operation | Syntax |
|---|---|
| Load | val.load() |
| Store | val.store(42) |
| Add | val.fetch_add(1) or val++ |
| Exchange | val.exchange(new_val) |
| CAS | val.compare_exchange_strong(exp, des) |
| Flag | flag.test_and_set() / flag.clear() |
Lesson Complete!
You've mastered atomic operations — the fastest synchronisation primitive in C++. Next: Memory Allocation Internals — how new/delete work under the hood and writing custom allocators.
Sign up for free to track which lessons you've completed and get learning reminders.