Skip to article
ALGORITHMICSConcurrency
Concurrency6 min read

Happens-Before

The relation that decides what one thread is allowed to see of another.


Thread A writes a value and sets a flag. Thread B waits for the flag, then reads the value.

// Thread A
data = 42;
ready = true;
// Thread B
while (!ready) {}
console.log(data); // 42? …not necessarily.

0 is a legal output. So is looping forever, even after A has finished.

Synchronisation

Thread A

data = 42;
ready = true;

Thread B

while (!ready) {}
print(data);
42 — the ordering happened to hold0 — B saw the flag before the datahangs — B hoisted the flag into a register

No happens-before edge exists between the two threads, so all three outcomes are legal. Your machine will pick the first one nearly every time, which is why this bug reaches production.

Why it can print zero

Two independent reasons, and both are the system working as designed.

The compiler reorders. data and ready are unrelated as far as it can tell, so it may emit the stores in either order. It may also hoist ready out of B’s loop into a register, making the loop infinite.

The CPU reorders. Stores sit in a write buffer and reach other cores in whatever order is convenient. Core B can observe ready = true before it observes data = 42.

The relation

Happens-before is a guarantee about visibility, and it is the only one you get:

If action X happens-before action Y, then everything X did is visible to Y.

If two actions are not ordered by it, they are concurrent, and neither is guaranteed to see the other. Notice what this is not: it is not about wall-clock time. A write that occurred earlier in real time is invisible unless a happens-before edge connects it.

Where the edges come from

Only from specific, named things:

Program order. Within one thread, earlier statements happen-before later ones. This is the guarantee that makes single-threaded code sane, and it is the only one you get for free.

Lock release → lock acquire. Everything before a release is visible after a subsequent acquire of the same lock.

Volatile / atomic write → read. Same shape, one variable.

Thread start. Everything before t.start() is visible to t.

Thread join. Everything t did is visible after t.join() returns.

Channel send → receive. In Go, a send happens-before the corresponding receive completes.

Transitivity. If X → Y and Y → Z, then X → Z. This is what makes the whole thing composable, and it is how most real edges are built: chains of small guarantees.

Fixing the example

// Thread A
data = 42;
ready.store(true, memory_order_release); // publish
// Thread B
while (!ready.load(memory_order_acquire)) {} // subscribe
console.log(data); // 42, guaranteed

The release-acquire pair creates the edge. The release says “everything I did before this is now published”; the acquire says “and I can see it”.

Crucially, data is an ordinary variable — the atomic operation on ready carries the ordering for everything written before it. That is why one atomic flag can safely publish a whole data structure.

Why this is worth learning once

Every concurrency primitive is a way of manufacturing happens-before edges. Once you see that, the rules stop being a list to memorise:

The relation is also the same idea that orders events in a distributed system — Lamport’s 1978 paper defines happens-before for messages between machines, and the memory-model version is that idea applied to cores. Vector clocks, Raft terms and causal consistency are all built on it.