Skip to content

Atomics and Synchronization — std::atomic, Memory Order, std::mutex, std::condition_variable, Lock-Free Programming

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Atomics and Synchronization. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ std::atomic provides lock-free atomic operations on fundamental types with memory ordering constraints, while std::mutex, std::condition_variable, and std::barrier provide higher-level synchronization for thread coordination.

What You'll Learn

You will use std::atomic for lock-free variables, understand memory ordering (relaxed, acquire, release, acq_rel, seq_cst), implement spinlocks and lock-free data structures, use std::mutex, std::shared_mutex, and std::condition_variable for thread synchronization, apply std::call_once for thread-safe initialization, and avoid deadlocks with std::lock and consistent ordering.

Why It Matters

Data races are undefined behavior in C++ — the compiler and hardware assume no races exist. Atomics provide the tools to write correct concurrent code without undefined behavior. C++'s memory model (C++11) defines exactly how threads interact through shared memory, enabling portable concurrent programming across all platforms.

Learning Path

graph LR
    A["62: Concurrency & Threads"] --> B["63: Atomics & Synchronization"]
    B --> C["64: File I/O & Serialization"]
    C --> D["65: Build Systems (CMake)"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic std::atomic

std::atomic<T> provides atomic operations on type T without explicit locking.

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter.fetch_add(1);  // Atomic increment
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

    for (auto& t : threads) t.join();

    std::cout << "Counter: " << counter.load() << "\n";  // 1000000
    // Without atomic: undefined behavior, likely wrong value

    // Common atomic operations
    std::atomic<int> value(10);

    int expected = 10;
    bool success = value.compare_exchange_weak(expected, 20);
    std::cout << "CAS success: " << success << "\n";  // true
    std::cout << "Value: " << value.load() << "\n";   // 20

    // Exchange
    int old = value.exchange(30);
    std::cout << "Old: " << old << ", New: " << value.load() << "\n";

    // Fetch-and-add/sub/and/or/xor
    std::atomic<int> flags(0);
    flags.fetch_or(1);   // Set bit 0
    flags.fetch_or(4);   // Set bit 2
    std::cout << "Flags: " << flags.load() << "\n";  // 5
}

Memory Ordering

The default memory order std::memory_order_seq_cst is correct but expensive. Weaker orders improve performance.

#include <iostream>
#include <atomic>
#include <thread>

// Relaxed ordering: only guarantees atomicity, no synchronization
std::atomic<int> relaxed_counter(0);
void relaxed_increment() {
    for (int i = 0; i < 1000; ++i) {
        relaxed_counter.fetch_add(1, std::memory_order_relaxed);
    }
}

// Release-Acquire ordering: synchronizes between threads
std::atomic<bool> ready(false);
std::atomic<int> data(0);

void producer() {
    data.store(42, std::memory_order_relaxed);       // Non-atomic data
    ready.store(true, std::memory_order_release);    // Publish
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {  // Wait for publish
        // Spin
    }
    std::cout << "Data: " << data.load(std::memory_order_relaxed) << "\n";
    // Guaranteed to see 42 because acquire synchronizes with release
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
}

Memory Order Reference

Order Purpose Cost
relaxed Atomicity only, no ordering Cheapest
consume Data dependency ordering (rarely used) Low
acquire Read: subsequent reads/writes can't move before Medium
release Write: preceding reads/writes can't move after Medium
acq_rel Read-modify-write: acquire + release combined Medium
seq_cst Sequential consistency: total global order Most expensive

Spinlock Implementation

A simple spinlock using std::atomic_flag (guaranteed lock-free).

#include <iostream>
#include <atomic>
#include <thread>
#include <vector>

class Spinlock {
    std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
    void lock() {
        while (flag_.test_and_set(std::memory_order_acquire)) {
            // Spin (busy-wait)
        }
    }

    void unlock() {
        flag_.clear(std::memory_order_release);
    }
};

Spinlock spin;
int shared_data = 0;

void worker() {
    for (int i = 0; i < 10000; ++i) {
        std::lock_guard<Spinlock> lock(spin);
        ++shared_data;
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 8; ++i) {
        threads.emplace_back(worker);
    }

    for (auto& t : threads) t.join();

    std::cout << "Shared data: " << shared_data << "\n";  // 80000
}

std::mutex and std::lock_guard

Mutexes provide blocking mutual exclusion.

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
#include <map>

class ThreadSafeMap {
    std::map<int, std::string> map_;
    mutable std::mutex mutex_;
public:
    void insert(int key, const std::string& value) {
        std::lock_guard<std::mutex> lock(mutex_);
        map_[key] = value;
    }

    std::string find(int key) const {
        std::lock_guard<std::mutex> lock(mutex_);
        auto it = map_.find(key);
        if (it != map_.end()) return it->second;
        return "not found";
    }

    // Multiple mutex operations with std::lock (deadlock-free)
    static void transfer(ThreadSafeMap& from, ThreadSafeMap& to,
                        int key, const std::string& value) {
        std::lock(from.mutex_, to.mutex_);
        std::lock_guard<std::mutex> lock1(from.mutex_, std::adopt_lock);
        std::lock_guard<std::mutex> lock2(to.mutex_, std::adopt_lock);

        from.map_.erase(key);
        to.map_[key] = value;
    }
};

int main() {
    ThreadSafeMap map;
    map.insert(1, "one");
    map.insert(2, "two");

    std::cout << map.find(1) << "\n";  // one
    std::cout << map.find(3) << "\n";  // not found
}

std::shared_mutex (Reader-Writer Lock)

Multiple readers can hold the lock simultaneously; writers have exclusive access.

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
#include <chrono>

class SharedData {
    int value_ = 0;
    mutable std::shared_mutex mutex_;
public:
    void write(int v) {
        std::unique_lock lock(mutex_);  // Exclusive access
        value_ = v;
        std::cout << "Writer set value to " << v << "\n";
    }

    int read() const {
        std::shared_lock lock(mutex_);  // Shared access
        return value_;
    }
};

int main() {
    SharedData data;

    // Multiple readers can read simultaneously
    auto reader = [&data](int id) {
        for (int i = 0; i < 5; ++i) {
            int val = data.read();
            std::cout << "Reader " << id << " saw " << val << "\n";
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    };

    auto writer = [&data]() {
        for (int i = 0; i < 3; ++i) {
            data.write(i * 100);
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }
    };

    std::thread w(writer);
    std::vector<std::thread> readers;
    for (int i = 0; i < 3; ++i) readers.emplace_back(reader, i);

    w.join();
    for (auto& r : readers) r.join();
}

std::condition_variable

Condition variables allow threads to wait for a condition to become true.

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <thread>
#include <chrono>

template <typename T>
class ThreadSafeQueue {
    std::queue<T> queue_;
    mutable std::mutex mutex_;
    std::condition_variable cv_;
public:
    void push(T value) {
        {
            std::lock_guard lock(mutex_);
            queue_.push(std::move(value));
        }
        cv_.notify_one();  // Wake one waiting consumer
    }

    T pop() {
        std::unique_lock lock(mutex_);
        cv_.wait(lock, [this]() { return !queue_.empty(); });
        T value = std::move(queue_.front());
        queue_.pop();
        return value;
    }

    bool tryPop(T& value) {
        std::lock_guard lock(mutex_);
        if (queue_.empty()) return false;
        value = std::move(queue_.front());
        queue_.pop();
        return true;
    }
};

int main() {
    ThreadSafeQueue<int> queue;

    std::thread producer([&queue]() {
        for (int i = 0; i < 5; ++i) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            queue.push(i);
            std::cout << "Produced: " << i << "\n";
        }
    });

    std::thread consumer([&queue]() {
        for (int i = 0; i < 5; ++i) {
            int value = queue.pop();
            std::cout << "Consumed: " << value << "\n";
        }
    });

    producer.join();
    consumer.join();
}

std::call_once for Thread-Safe Initialization

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

class LazyResource {
    std::once_flag initFlag_;
    std::string data_;

    void initialize() {
        std::cout << "Initializing resource (once)\n";
        data_ = "Expensive-to-create resource";
    }

public:
    const std::string& getData() {
        std::call_once(initFlag_, &LazyResource::initialize, this);
        return data_;
    }
};

int main() {
    LazyResource resource;
    std::vector<std::thread> threads;

    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([&resource, i]() {
            std::cout << "Thread " << i << ": " << resource.getData() << "\n";
        });
    }

    for (auto& t : threads) t.join();
    // "Initializing resource (once)" appears exactly once
}

Lock-Free Stack (Simplified)

A simple lock-free stack using atomic operations.

#include <iostream>
#include <atomic>
#include <thread>

template <typename T>
class LockFreeStack {
    struct Node {
        T value;
        Node* next;
    };

    std::atomic<Node*> head_{nullptr};

public:
    void push(T value) {
        Node* node = new Node{std::move(value), nullptr};
        node->next = head_.load(std::memory_order_relaxed);
        while (!head_.compare_exchange_weak(
            node->next, node,
            std::memory_order_release,
            std::memory_order_relaxed)) {
            // CAS failed: node->next already updated by compare_exchange_weak
        }
    }

    bool pop(T& value) {
        Node* node = head_.load(std::memory_order_acquire);
        while (node && !head_.compare_exchange_weak(
            node, node->next,
            std::memory_order_release,
            std::memory_order_relaxed)) {
            // Retry
        }
        if (!node) return false;
        value = std::move(node->value);
        delete node;  // Note: ABA problem ignored for simplicity
        return true;
    }
};

int main() {
    LockFreeStack<int> stack;

    std::thread producer([&stack]() {
        for (int i = 0; i < 1000; ++i) stack.push(i);
    });

    std::thread consumer([&stack]() {
        int value;
        for (int i = 0; i < 1000; ++i) {
            while (!stack.pop(value)) {
                std::this_thread::yield();
            }
        }
    });

    producer.join();
    consumer.join();
    std::cout << "Lock-free stack test passed\n";
}

Common Mistakes

Mistake 1: Race condition with mutex

if (!queue.empty()) {  // Can change between check and pop!
    auto val = queue.front();
    queue.pop();
}

Use std::condition_variable or lock the entire check+pop sequence.

Mistake 2: Forgetting volatile for non-atomic shared flags

bool ready = false;  // Compiler may optimize away the read

Use std::atomic<bool> instead. volatile is not sufficient for thread synchronization.

Mistake 3: Using relaxed ordering when acquire/release is needed

std::atomic<bool> flag;
flag.store(true, std::memory_order_relaxed);  // Other thread may never see it

Use release/acquire for signaling between threads.

Mistake 4: Deadlock with multiple mutexes

Lock mutexes in a consistent order or use std::lock(m1, m2).

Mistake 5: Spurious wakeup without predicate in condition_variable

cv.wait(lock);  // May wake up without notification
cv.wait(lock, []{ return condition; });  // Safe with predicate

Practice Questions

  1. What does std::memory_order_acquire guarantee? Answer: No reads or writes after the acquire can be reordered before the acquire operation.

  2. What is the difference between std::mutex and std::shared_mutex? Answer: mutex is exclusive-only. shared_mutex allows multiple concurrent readers with exclusive writers.

  3. What is a spinlock and when would you use it? Answer: A spinlock busy-waits in a tight loop. Use it for very short critical sections to avoid OS context switch overhead.

  4. How does compare_exchange_weak differ from compare_exchange_strong? Answer: weak may spuriously fail (return false even when expected matches), allowing better Code Generation in loops. Use weak in loops, strong for single attempts.

  5. What is the ABA problem in lock-free programming? Answer: A pointer value changes from A to B and back to A. The CAS cannot detect the intermediate change. Use tagged pointers or hazard pointers.

FAQ

What is std::atomic in C++

std::atomic provides lock-free atomic operations on fundamental types. Operations like load, store, fetch_add, and CAS are indivisible across threads.

What is memory ordering in C++

Memory ordering controls how atomic operations synchronize with other threads. The default seq_cst provides sequential consistency; relaxed provides only atomicity.

When should I use std::mutex vs std::atomic

Use std::mutex for complex critical sections and when you need to wait (condition_variable). Use std::atomic for simple counters, flags, and lock-free data structures.

What is a condition_variable used for

condition_variable enables threads to wait efficiently for a condition to be true, avoiding busy-waiting. Combined with mutex and predicate, it's the core of producer-consumer patterns.

Is lock-free programming always faster

No. Lock-free code uses CAS loops that can spin under contention. For most use cases, a well-designed mutex is both simpler and faster.

Mini Project

Implement a thread-safe, multi-producer, multi-consumer (MPMC) queue using std::mutex and std::condition_variable:

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <string>

// Your MPMC queue

int main() {
    MPMCQueue<std::string> queue(10);  // Max 10 items

    // 2 producers, 3 consumers
    std::vector<std::thread> producers;
    std::vector<std::thread> consumers;

    for (int i = 0; i < 2; ++i) {
        producers.emplace_back([&queue, i]() {
            for (int j = 0; j < 5; ++j) {
                std::string msg = "P" + std::to_string(i) + "-" + std::to_string(j);
                queue.push(msg);
                std::this_thread::sleep_for(std::chrono::milliseconds(20));
            }
        });
    }

    std::atomic<int> consumed(0);
    for (int i = 0; i < 3; ++i) {
        consumers.emplace_back([&queue, &consumed]() {
            std::string msg;
            while (consumed < 10) {
                if (queue.pop(msg, std::chrono::milliseconds(100))) {
                    std::cout << "Consumed: " << msg << "\n";
                    ++consumed;
                }
            }
        });
    }

    for (auto& t : producers) t.join();
    for (auto& t : consumers) t.join();

    std::cout << "All " << consumed << " messages processed\n";
}

This project demonstrates real-world C++ synchronization used in thread pools, task schedulers, and message passing systems. Compare with Java's BlockingQueue interface.

What's Next

You now understand atomics and synchronization — the building blocks of concurrent C++. Next, you will learn file I/O and Serialization, covering std::fstream, binary vs text I/O, and serialization formats like JSON and binary archives.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro