5.2.27 · D1C++ Programming

Foundations — Memory model — happens-before, acquire-release semantics

2,360 words11 min readBack to topic

Before you can read the parent note Memory Model, you need every word and symbol it silently assumes. We build them one at a time, from nothing, each on top of the last.


0. What is "memory" here — the picture we all share

Figure — Memory model — happens-before, acquire-release semantics

Look at the figure: two shelves (two CPU cores), and boxes on each. The key trouble — which this whole topic is about — is that when core 1 changes a box, core 2 does not see the change instantly. There's a delay, drawn as the wavy pipe between them.


1. Thread — a worker with its own to-do list

The picture: two people (Thread 1, Thread 2) each holding a to-do list, both reaching for the same boxes. Everything hard in this topic happens because they share memory but run at their own pace.

Why the topic needs it: a data race, happens-before, acquire, release — all of these describe relationships between two threads. With one thread there is no problem at all.


2. Program order — top-to-bottom, as written

The picture: an arrow pointing straight down the to-do list. This is the order you wrote, not necessarily the order the machine does things — which is the whole surprise of the next symbol.


3. Reordering — the surprise

Figure — Memory model — happens-before, acquire-release semantics

In the figure the black arrow is the order you wrote; the red arrow is an order the machine might actually run. Three separate villains can cause this swap:


4. std::atomic<T> — an indivisible box

The picture: a box with a solid lid, so anyone looking in sees either the old value or the new value, never a smeared mix. Important and easy to miss: atomic guarantees this only for the atomic box itself — it says nothing yet about the ordinary boxes around it. Ordering those is a separate job (that's what §9–11 are for).


5. .store(), .load(), and RMW — the three verbs

The picture: store = dropping a note in; load = peeking in; RMW = grabbing the note, editing it, and putting it back before anyone else can touch the box.


6. Data race — the thing we must avoid

Why the topic needs it: the entire memory model exists to let you avoid data races by supplying the missing ordering arrow. Everything from here is about building that arrow correctly.


7. Acquire and release — the two ends of the handshake

We must meet these two words before we can define the cross-thread arrow, because that arrow is made of them.

Figure — Memory model — happens-before, acquire-release semantics

The figure shows release as a floor (⬇ things can't fall through) and acquire as a ceiling (⬆ things can't rise through). Together, when the acquire-load reads exactly the value the release-store wrote, a handshake forms — the cross-thread arrow of the next section.

A mutex is just these two glued together: locking is an acquire, unlocking is a release.


8. The three ordering relations — arrows between events

An "event" here just means one read or write happening. We connect events with arrows. First, two tiny pieces of notation so the arrows read cleanly:

That formula is the payoff. If — and only if — you can draw a happens-before path from a write to a read, the read is guaranteed to see that write. No path ⇒ no guarantee ⇒ possible data race.

Figure — Memory model — happens-before, acquire-release semantics

Follow the figure: the solid down-arrows are sequenced-before, the dashed cross-arrow is synchronizes-with, and the long green path from (1) to (4) is the happens-before chain that makes data == 42 safe.


9. memory_order — the strength dial

Picture: a dial from "loose" (relaxed) to "strict" (seq_cst). More strictness = more guarantees = usually slower, because the CPU must insert real fences (see std-atomic_thread_fence).


Prerequisite map

Memory location and threads

Store buffer SB delay

Reordering plus as-if rule

std atomic box no tearing

Per variable coherence

Data race is UB

store load RMW

Acquire and Release gates

Synchronizes with handshake

Release sequence baton

Happens before arrow

Sequenced before program order

Memory model guarantee

memory order dial

Each foundation feeds the next; the two arrows that finally meet at Happens-before are the whole point — one from inside a thread (sequenced-before), one across threads (synchronizes-with).


Equipment checklist

Test yourself — but each line here also pushes one step past the plain definition, so read them as mini-puzzles, not repeats.

Two threads each store a different value to the same atomic box; can they disagree on which store was last?
No — per-variable coherence forces a single modification order all threads agree on.
A store buffer delays a write; does that delay ever affect the thread that made the write?
No — a thread always reads its own latest write; the delay is only visible to other cores.
An acquire-load reads a value written not by the release-store but by a later RMW on that box — is the handshake still made?
Yes — the RMW is part of the release sequence, so it carries the same baton.
You tag a load consume to save time; what will a modern compiler actually emit?
acquire — every current compiler promotes consume to acquire.
relaxed gives per-variable coherence but not what?
any ordering arrow relating other boxes to this one — so no happens-before for surrounding data.
Why must acquire and release be defined before synchronizes-with?
because synchronizes-with is literally built out of a release store being read by an acquire load.
read aloud is
if A happens-before B, B is guaranteed to observe all of A's writes.