Concurrency
Part 5 of 6 · ConcurrencyHappens-Before & Memory Visibility
Happens-before (≺) is the relation that makes writes visible and ordered across threads. Mutex unlock ≺ later lock; volatile/atomic stores synchronize with later loads; thread start/join create edges. Without a happens-before edge, seeing stale/torn values is allowed.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Overview
CPUs and compilers reorder. Caches are coherent but not magic publication. The language memory model tells you which writes a thread must see. That relation is happens-before (hb / ≺). Interviews fail people who treat time as visibility.
By the end you should be able to:
- List the common hb edges
- Publish two fields under one mutex (or one atomic flag with a matching order)
- Explain a data race on a plain
boolean running - Contrast JMM
volatilewith C++memory_order - Refuse
sleep(1)as a barrier
Publishing x and y to another thread
Prefer
Write both under mutex; reader locks the same mutex
Unlock synchronizes-with lock. The reader sees both writes or waits. Invariant lives in one critical section.
- Same mutex. Different mutexes do not create this edge.
- RWLock write-unlock ≺ later read-lock also works.
- Atomics: store the payload, then a release flag; load acquire the flag, then payload.
Alternative
Plain x=; y=; running=true without sync
The reader can see running and a torn x, or neither, or y without x. Wall clock said the writer finished.
- This is a data race in C++ / a JMM race if running is not volatile.
- sleep on the reader is not hb.
- volatile running alone still does not make x++ atomic.
Establishing an edge
If you cannot name the edge, the read is allowed to be stale.
- 1
Program order in one thread
A sequenced-before B in the same thread ⇒ A ≺ B. - 2
Unlock ≺ later lock (same mutex)
Everything before unlock is visible after lock. - 3
Release store ≺ acquire load (same atomic)
C++/Rust. JMM volatile is seq-cst-ish for that variable. - 4
start / join / CV reacquire
Parent ≺ child start; child end ≺ join. Wait returns with the mutex. - 5
No edge: two threads, plain fields
Stale/torn/reordered is legal. Time is not a proof.
Edges worth memorizing
JMM / C++11-style (same ideas, different names):
| Edge | Example |
|---|---|
| Sequenced-before | Same thread, source order |
| Unlock → lock | mu.unlock() ≺ later mu.lock() |
| Volatile/atomic write → later read | JMM volatile; C++ release/acquire |
| Thread start | Parent start() ≺ first action in child |
| Join | Last action in child ≺ parent after join |
| CV | Wait’s reacquire after a notify that ran with the mutex |
Happens-before is transitive. Unlock ≺ lock ≺ later reads in the acquirer.
It is not total. Concurrent actions may be unordered. Then the model allows surprising views.
Data race
C++: two accesses to one location, at least one write, not both atomic, not ordered by hb → undefined behavior.
JMM: a similar race makes execution not sequentially consistent; you can see stale caches of fields.
Fix: mutex, or atomic with a sufficient order, or don’t share.
Torn reads: long / struct stored in two pieces on some platforms; without sync you can see a mix. Two fields x,y without a pair-wise publish is the interview picture: reader sees new x and old y.
volatile / atomics ≠ mutex
volatile (Java) / atomic load-store: visibility and ordering of that location. count++ is still read-modify-write: two threads lose updates. Compound invariants (len == buf.length) need a lock or a carefully reviewed lock-free algorithm. Next page: atomics vs locks.
C++ memory_order_relaxed: atomicity of the word, no hb. release / acquire pair: payload visibility. seq_cst: total order of seq_cst ops — default you should keep until you can prove otherwise.
Sleep is not a barrier
sleep, print, “it works on my laptop” introduce timing, not hb. They hide races. TSAN / the JMM rules still apply.
Flow
- 1
Writer: x=1; unlock mu
- hbReader: lock mu; read x
- 2
Reader: lock mu; read x
- 3
Writer: x=1; no sync
- no hbReader: read x — stale allowed
- 4
Reader: read x — stale allowed
Lesson map
Happens-Before & Memory Visibility
Happens-before (≺) is the relation that makes writes visible and ordered across threads. Mutex unlock ≺ later lock; volatile/atomic stores synchronize with later loads; thread start/join create edges. Without a happens-before edge, seeing stale/torn values is allowed.
Architecture. Architecture
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB w["Writer: x=1 unlock mu"] l["Reader: lock mu read x"] w2["Writer: x=1 no sync"] r2["Reader: read x - stale allowed"] w -->|hb| l w2 -->|no hb| r2
Publish protocol (run this)
Press Run. Snippets must be self-contained — no network, files, or native modules.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Interview Q&A
What does happens-before buy you?
Answer
If A ≺ B, A’s writes are visible to B and A is ordered before B. Without an edge, stale or torn views are allowed even if clocks say A finished first.
Mutex visibility rule?
Answer
Unlock synchronizes-with a later lock on the same mutex. Payload written before unlock is visible after lock.
Does volatile make count++ safe?
Answer
No. It orders individual accesses to that variable. The RMW is still a race. Use atomic RMW or a lock.
Is wall-clock order happens-before?
Answer
No. Time is not in the memory model. sleep is not a barrier.
Two mutexes — does unlocking A publish to someone locking B?
Answer
No. Same lock (or a chain of hb edges). Different mutexes are different sync objects.
C++ relaxed vs acquire/release?
Answer
Relaxed: atomic word, no hb. Release store synced with acquire load on the same variable publishes prior writes. seq_cst is stronger and simpler.
Thread join?
Answer
After join returns, the parent has hb from the child’s last actions. You may read the child’s results without extra fences.
CV wait?
Answer
Wait atomically drops the mutex and later reacquires it. The reacquire is a lock, so it syncs with the corresponding unlock/notify protocol. Still while (!pred) — Mesa.
Data race in C++?
Answer
UB. “It printed 1 in the test” does not refute UB. Use atomics or mutexes.
Can I use a plain boolean stop flag?
Answer
Not portably. Make it atomic/volatile (Java) with a real order, or interrupt/join. Otherwise the worker may never see stop.
Pitfalls
Write x, y, ready published to a reader. Show the mutex protocol and the release/acquire protocol. List three views allowed if ready is a plain bool. If you used sleep, delete it.
Go Deeper
Cluster: deadlock · next atomics vs locks