Skip to content

Atomics and the Memory Model

Beginning of the lesson

Atomics and the Memory Model

Sharing data between threads without locks, correctly

Big Idea: When two threads touch the same variable and at least one writes, an ordinary read/write is a data race - and a data race in C++ is undefined behavior, not merely a wrong value. std::atomic<T> gives you operations that are indivisible (no thread ever sees a half-written value) and, just as importantly, lets you control the ordering in which one thread's writes become visible to another. This topic is about both halves: atomicity and the memory model that defines "visible".
By the end you will be able to:
  • state precisely what a data race is and why it is undefined behavior;
  • use std::atomic<T> - load, store, exchange, fetch_add, compare_exchange;
  • explain the happens-before relationship and how it makes a write visible;
  • reason at an intuition level about the memory orderings seq_cst, acquire/release, and relaxed.

1. Data races are undefined behavior

A data race occurs when two threads access the same memory location concurrently, at least one access is a write, and the accesses are not ordered by any synchronization. The C++ standard says a program with a data race has undefined behavior (UB). That is stronger than "you might read a stale value" - the compiler and CPU are allowed to assume races never happen, so a race can produce torn values, lost updates, or code that is optimized in ways that make no sequential sense.

int counter = 0;                 // plain int, shared

void worker() {
    for (int i = 0; i < 100000; ++i)
        ++counter;               // read-modify-write, NOT atomic -> DATA RACE
}
// Two threads running worker() race on counter.
// The final value is unpredictable and the program is UB.

++counter looks like one step but is really three: load, add one, store. Two threads can both load the same value, both add one, and both store - so two increments become one. Making counter an int guarded by nothing does not just risk a wrong total; it is a race, and races are UB.

Two threads that only read the same variable never race. A race needs at least one writer with no synchronization. Data protected by a std::mutex is not racing either - the lock provides the ordering.

Continue this lesson on YourAIZone

The full lesson has the complete explanation, worked examples, common mistakes, tests with instant checking and an AI tutor. Start right away as a guest, or create an account to keep your progress.

In the full lesson

  • The complete explanation
  • Worked examples
  • Common mistakes and how to avoid them
  • Test questions: 15
  • AI tutor that answers questions about the topic
  • Progress tracking for students, parents and teachers