What "atomic" actually buys you
An atomic operation makes two separate promises, and it is worth pulling them apart because beginners blur them. The first is indivisibility: an atomic read or write of a value happens all at once, with no half-written state ever visible to another thread. On a 64-bit machine a plain unsigned long write is usually indivisible already, but a wider type, or a plain variable the compiler split into two stores, gives no such guarantee — an atomic makes it a rule rather than an accident. The second promise is the one this whole rung is about: an atomic operation can carry an ordering guarantee about the other, ordinary memory around it. The first promise alone would be nearly useless; it is the second that lets you build correct communication between threads.
In C and C++ you reach for an atomic type to get this. You declare a counter as an atomic int rather than a plain int, and from then on every load and store of it goes through the atomic machinery instead of being a bare memory access. The crucial mental shift from the last guide: the data-race-free guarantee said a plain shared variable that two threads touch (one writing) is undefined behavior. An atomic is precisely the escape hatch — two threads racing on an atomic is defined, well-behaved, and exactly what the model intends. Reaching for atomics is how you say to the compiler and CPU, "this location is shared on purpose; do not pretend otherwise."
The small menu of atomic operations
An atomic offers a short, fixed menu, and almost everything you will ever do is one of these. There are the two obvious ones: an atomic load reads the current value, and an atomic store writes a new one. Then there is exchange — write a new value and hand back the old one, in a single indivisible step. And then the family this rung keeps circling back to: the read-modify-write operations, which read, compute, and write back as one inseparable unit so that no other thread can slip in between the read and the write. fetch_add (add and return the previous value) is the everyday example; the compare-and-swap that the next guides lean on is the powerful one.
Why does read-modify-write need to be atomic at all? Picture two threads each doing counter = counter + 1 on a plain int that starts at 0. You met this exact hazard in the synchronization rung. Thread A reads 0; before it writes back, thread B also reads 0; both compute 1; both store 1. Two increments, one lost — the result is 1, not 2. An atomic fetch_add closes that gap: the read and the write are welded together, so when B's add runs it is guaranteed to see A's result. Every concurrent counter, reference count, and lock you will ever build rests on this single welded read-write.
There is a quiet guarantee underneath all of this worth naming now, because guide 3 will need it. For one single atomic location, all the stores to it form one agreed-upon total order that every thread sees consistently — the modification order of that variable. Threads may disagree about the relative order of writes to different locations (that was the whole store-buffering shock), but for any one atomic, everyone agrees on the sequence of values it took. That per-location agreement is the bedrock the stronger orderings are built on top of.
The default that always works: sequential consistency
Every atomic operation takes an optional argument — a value from the memory_order enum — that selects how much surrounding ordering it carries. If you write none, you get the strongest one for free: memory_order_seq_cst, which delivers sequential consistency. This is the single honest notebook from the last guide, handed back to you as a guarantee. Under seq_cst there exists one global order of all your atomic operations that every thread agrees on, and it respects each thread's program order. It is, deliberately, the default you fall into if you do not think hard — and that is a kindness, because seq_cst is the only order that matches the intuition most people already have.
Recall the store-buffering example: two threads each write one variable then read the other, and on real hardware both reads can come out 0. Make those four operations seq_cst atomics and that outcome becomes forbidden. There must be a single global order, so one of the writes provably comes first, and the read that follows it in that order must see 1. The compiler and CPU honor this by inserting the hardware fences needed to drain store buffers at the right moments. You did not have to know about store buffers; you asked for seq_cst and the platform paid whatever it costs to make your intuition true.
atomic int x = 0, y = 0; Thread 1 Thread 2 x.store(1); y.store(1); r1 = y.load(); r2 = x.load(); seq_cst (the default): r1 == 0 and r2 == 0 is FORBIDDEN relaxed: r1 == 0 and r2 == 0 is allowed again
Turning the dial down: the relaxed end
If seq_cst always works, why is there a dial at all? Because that global agreement is expensive, especially on weak hardware, and often you are paying for ordering you do not actually need. The weakest setting is memory_order_relaxed. A relaxed atomic still keeps the first promise — indivisibility, and the per-location modification order — but it makes no promise about ordering relative to any other memory. It says only: this one location's reads and writes are atomic and consistently ordered among themselves; everything else around them is free to be reordered as before.
The honest, narrow use for relaxed is a counter whose value you care about but whose timing relative to other data you do not. A statistics counter bumped from many threads is the textbook case: use an atomic fetch_add with relaxed, and every increment still counts exactly once (the read-modify-write is still welded), but you spend nothing forcing it into a global order with unrelated writes. The trap is using relaxed as a flag that is supposed to publish other data — "set ready = 1 so the reader knows the buffer is filled." Relaxed gives you no guarantee that the buffer's writes are visible when the reader sees ready, so that pattern is broken. Publishing data is the job of acquire and release, the very next guide.
The full family, laid out in order of strength
Now we can lay the whole memory_order family out as one spectrum, from weakest to strongest, so the names stop being a soup. Each step up adds a guarantee and a cost. The crucial middle pair — acquire and release — is where most real lock-free code lives, and it is exactly what guide 3 unpacks in full; here we just place it on the map so you see the shape of the whole thing.
- memory_order_relaxed — atomicity and per-location modification order only. No ordering with any other memory. Use for free-standing counters where only the final tally matters.
- memory_order_consume — a weaker, dependency-based cousin of acquire. It was meant to order only reads that depend on the loaded pointer, but it proved so hard to specify and implement that current compilers quietly promote it to acquire. Honestly: treat it as a historical footnote, not a tool you should pick.
- memory_order_acquire (on a load) and memory_order_release (on a store) — the workhorse pair. A release store publishes everything written before it; a matching acquire load that reads that value sees all of it. This is the one-way handshake that lets one thread hand data to another safely, and it is the whole subject of the next guide.
- memory_order_acq_rel — for a single read-modify-write that needs both halves at once: it acquires on the read side and releases on the write side. This is what you put on a compare-and-swap inside a lock or a queue.
- memory_order_seq_cst — everything acq_rel gives, plus membership in one single global total order that all threads agree on. The strongest, the default, the one that restores the single-notebook picture and the only one most code should ever use.
Hold this shape in your head and the rest of the rung snaps into place. An atomic gives indivisibility plus a tunable ordering; the memory_order argument is the dial; seq_cst is the safe top of the dial and the default; relaxed is the bare bottom; and the acquire/release pair in the middle is where data actually gets handed between threads. Guide 3 turns that handshake into the precise relations of synchronizes-with and happens-before; guide 4 adds standalone fences and looks hard at read-modify-write; guide 5 shows why x86 charges you almost nothing for acquire/release while a weak ARM core charges real cycles. The dial is the same the whole way up — you are just learning which setting each problem truly needs.