Concept of the day · concurrency

Happens-Before and Memory Barriers

Without a barrier, there is no guarantee another thread ever sees your write.

The mechanism. Compilers reorder instructions, and CPUs reorder memory operations and buffer stores. Both are legal because single-threaded semantics are preserved. Across threads, that guarantee evaporates. A memory model defines happens-before: a partial order such that if write W happens-before read R, R must observe W. Synchronisation operations — a lock release/acquire pair, a volatile write/read, an atomic store-release/load-acquire — create those edges. Nothing else does.

The bug. A worker sets done = true and the reader loops while (!done) {}. Without volatile or an atomic, the compiler may hoist the read out of the loop entirely and the loop never terminates. This is not a theoretical risk; it is standard optimisation. Double-checked locking without volatile has the same flavour: another thread can see a non-null reference to a partially constructed object.

The orderings worth knowing. Relaxed gives atomicity only, no ordering. Acquire on a load and release on a store together give you the publish/subscribe edge you almost always want — everything written before the release is visible after the acquire. SeqCst adds a single global order and costs a full fence. Use acquire/release; reach for relaxed only for counters whose value you read once at the end.

Data race is undefined behaviour. In C and C++ this is not a figure of speech: a racy program has no defined meaning at all, and the compiler may do anything. Java and Go define racy outcomes more narrowly but still permit values you would call impossible.

Takeaway. "It works on my machine" and "it works on x86" are the same statement, and neither survives an ARM server or a new compiler version.

← edition of 2026-11-03cheatsheet