Atomic Operations
Master lock-free thread-safe operations using atomic types for high-performance concurrent programming
Learn how to perform lock-free thread-safe operations using atomic types, enabling high-performance concurrent programming without mutexes.
A Simple Example
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> counter{0};
std::atomic<bool> ready{false};
std::atomic<int> data{0};
void incrementCounter(int iterations) {
for (int i{0}; i < iterations; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
void writer() {
data.store(42, std::memory_order_relaxed);
ready.store(true, std::memory_order_release); // Synchronizes with acquire
}
void reader() {
while (!ready.load(std::memory_order_acquire)) {
// Spin until ready
}
std::cout << "Data: " << data.load(std::memory_order_relaxed) << "\n";
}
int main() {
// Example 1: Lock-free counter
const int iterations{10000};
std::vector<std::thread> threads;
for (int i{0}; i < 4; ++i) {
threads.emplace_back(incrementCounter, iterations);
}
for (auto& thread : threads) {
thread.join();
}
std::cout << "Counter: " << counter.load() << "\n";
std::cout << "Expected: " << (4 * iterations) << "\n\n";
// Example 2: Lock-free synchronization
std::thread writerThread{writer};
std::thread readerThread{reader};
writerThread.join();
readerThread.join();
return 0;
}
Breaking It Down
std::atomic basics
- What it does: Makes operations on a variable indivisible and thread-safe
- load(): Read the value atomically
- store(): Write a value atomically
- Remember: Works for integers, bools, pointers, and simple types
Atomic operations
- fetch_add(): Add and return old value atomically
- fetch_sub(): Subtract and return old value atomically
- compare_exchange_weak(): Compare and swap if equal (can fail spuriously)
- Remember: These are lock-free and faster than mutex for simple operations
Memory ordering
- memory_order_relaxed: No ordering guarantees, fastest
- memory_order_acquire/release: Synchronizes with paired operations
- memory_order_seq_cst: Strongest guarantee, default, slowest
- Remember: Use relaxed for independent updates, acquire/release for synchronization
When to use atomics vs mutexes
- Use atomics: Simple counters, flags, single variable updates
- Use mutexes: Multiple variables, complex operations, critical sections
- Trade-off: Atomics are faster but limited to simple operations
- Remember: Do not prematurely optimize - measure before choosing
Why This Matters
- Mutexes have overhead - context switching, blocking, and cache effects. Atomic operations are faster for simple shared data.
- Lock-free algorithms avoid deadlocks, priority inversion, and blocking, making them ideal for real-time systems.
- Counters, flags, and simple shared state benefit from atomic operations instead of heavyweight mutex locking.
Critical Insight
Atomic operations are like vending machines. You insert money and press a button - the whole transaction happens as one indivisible operation. No one can interrupt halfway through and take your snack.
Memory ordering is about what other threads can see before and after your atomic operation. memory_order_relaxed is like not caring what others see. memory_order_release/acquire is like shouting "I am done!" so others know they can safely read your results.
Best Practices
Start with seq_cst: Use the default memory_order_seq_cst until profiling shows it is a bottleneck.
Use atomics for simple data: Counters, flags, and single variables are perfect for atomics. Complex operations need mutexes.
Pair acquire and release: When synchronizing between threads, use release in the writer and acquire in the reader.
Use relaxed for independent updates: If each update is independent, memory_order_relaxed is safe and fastest.
Do not guess memory ordering: Memory ordering is subtle. Use seq_cst unless you have measured and understand the trade-offs.
Common Mistakes
Using relaxed when you need synchronization: memory_order_relaxed does not synchronize memory between threads.
Overusing atomics: Complex operations still need mutexes. Do not try to build everything with atomics.
Assuming atomics are always faster: For simple cases yes, but complex atomic algorithms can be slower than mutexes.
Incorrect memory ordering pairs: release must be paired with acquire, not relaxed.
Debug Challenge
This atomic operation needs the correct memory ordering for synchronization. Click the highlighted line to fix it:
Quick Quiz
- When should you use atomics instead of mutexes?
- What does memory_order_relaxed mean?
- What is the default memory order for atomic operations?
Practice Playground
Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.
Output:
Error:
Lesson Progress
- Fix This Code
- Quick Quiz
- Practice Playground - run once