Courses/C++/Atomic Operations

    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

    Try it Yourself »
    C++
    #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

    Try it Yourself »
    C++
    #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

    Try it Yourself »
    C++
    #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

    OperationSyntax
    Loadval.load()
    Storeval.store(42)
    Addval.fetch_add(1) or val++
    Exchangeval.exchange(new_val)
    CASval.compare_exchange_strong(exp, des)
    Flagflag.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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service