Good to see you again. Last lesson established the basic publication pattern: ordinary payload writes become safe to consume only when a producer performs a release operation and a consumer performs a matching acquire operation on the same atomic flag.
This lesson makes that pattern operational. You will read short concurrent traces—litmus tests—and decide which register outcomes are possible on a weakly ordered system. The central skill is not memorizing a particular ARM barrier instruction. It is identifying whether a particular observed load creates the release-to-acquire synchronization edge that the trace needs.
First, separate the three levels of reasoning
Memory-ordering discussions become confusing when code, compiler behavior, CPU behavior, and devices are all mixed together. For interview traces, begin by identifying the level being tested.
-
C11/C++ atomics: The source language defines what executions are allowed. Acquire and release constrain compiler transformations and cause the compiler to generate suitable CPU ordering instructions where needed.
-
Linux kernel primitives:
smp_store_release()andsmp_load_acquire()express a comparable publish/consume pattern under the Linux Kernel Memory Model. They are not merely syntax variants of C11 atomics, but the reasoning shape is familiar. -
Raw hardware litmus tests: These show direct loads, stores, and explicit barriers, with no compiler involved. On ARM, independent accesses to normal memory may be observed in a different order from program order unless dependencies or barriers constrain them.
For a driver interview, state your assumption explicitly:
“I am treating this as a data-race-free C11 atomic trace”
or
“This is an assembly-level ARM trace, so I am reasoning about CPU ordering rather than C undefined behavior.”
That distinction matters. A trace using plain shared C variables concurrently is not a useful C11 memory-order puzzle: it is normally a data race and therefore undefined behavior.
The minimal model: local order, coherence, and conditional synchronization
Here is the essential acquire-release model.
A release store prevents earlier operations in its own thread from being published after that store. An acquire load prevents later operations in its own thread from being performed as if they occurred before that load.
But those local constraints become a cross-thread guarantee only under one condition:
The acquire load must read the value written by the release store, or by its applicable release sequence, on the same atomic object.
When that happens:
The middle arrow is the important one: synchronizes-with. The full chain creates happens-before.
In contrast, these do not create a synchronization edge:
- A release store to
readyand an acquire load from some unrelatedstatus. - An acquire load that reads an older value, such as the initial zero.
- A release and acquire merely appearing somewhere in the same program without the acquire observing that release.
The following Linux-kernel documentation is worth reading now because it states the two directional guarantees precisely, then applies them to a compact two-task trace.
Read the Linux kernel documentation’s “Release Operations” and “Acquire Operations” sections. It gives a useful driver-oriented formulation: release orders prior accesses before its store, and acquire orders its load before subsequent accesses.
In “Release Operations,” read the release explanation. Then read the “Acquire Operations” section through its first example. Focus especially on the task0() / task1() example. Read the two-task trace. Translate its final statement into an outcome rule: seeing y == 1 through the acquire load rules out later seeing the older value of x.
The kernel notation in that example maps conceptually to the source-level pattern you used last lesson:
| C11 | Typical Linux kernel counterpart | Intent |
|---|---|---|
atomic_store_explicit(&y, 1, memory_order_release) |
smp_store_release(&y, 1) |
Publish earlier work before setting y. |
atomic_load_explicit(&y, memory_order_acquire) |
smp_load_acquire(&y) |
Consume publication after observing y. |
| ordinary payload access | READ_ONCE() / WRITE_ONCE() plus ordering primitive where appropriate |
Access the data governed by the protocol. |
READ_ONCE() and WRITE_ONCE() suppress problematic compiler transformations; by themselves, they do not provide the required CPU ordering on weakly ordered systems. The acquire or release operation carries that ordering role.
A trace-analysis procedure you can use under pressure
For each trace, use the same sequence.
1. Record the initial state and label operations
Assume the shared variables start at zero unless the problem says otherwise. Label each load result and each relevant store:
- : write
1tox - : read
0fromy - : release-store
1 - : acquire-load observing
1
This keeps a multithreaded listing from becoming visually ambiguous.
2. Add each thread’s program-order constraints
Within a thread, source operations have a local sequence. For acquire/release reasoning, focus on the side each operation constrains:
- Before a release store: operations are on the published side.
- After an acquire load: operations are on the consuming side.
3. Ask what each load read from
This is the decisive step. A load reading the initial value does not synchronize with a later release store. A load reading a release-written value does.
4. Draw happens-before only where justified
Do not draw a release-to-acquire arrow because the operations have compatible names. Draw it only when the acquire reads the relevant release value.
5. Test the proposed outcome against those edges
An outcome is forbidden if it requires a read to observe an older value even though the write of the newer value happens-before that read.
A useful verbal conclusion is:
“This outcome is forbidden because the acquire reads the release-published flag, so the payload store happens-before the payload load.”
Or, for a permitted result:
“This outcome is allowed because neither acquire load observes the other thread’s release store. There is no cross-variable synchronization edge and no single global order imposed by acquire-release.”
Trace 1: publication forbids “new flag, old payload”
Consider the one-shot message publication pattern in C11 form:
#include <stdatomic.h>
int payload = 0;
atomic_int ready = 0;
/* Producer */
payload = 42;
atomic_store_explicit(&ready, 1, memory_order_release);
/* Consumer */
int r = atomic_load_explicit(&ready, memory_order_acquire);
int v = -1;
if (r == 1)
v = payload;
The consumer can report either of these meaningful results:
| Observed result | Permitted? | Reason |
|---|---|---|
r == 0 |
Yes | The consumer did not observe the producer’s publication and does not consume the payload. |
r == 1, v == 42 |
Yes | The acquire load observes the release store; the payload was published before it. |
r == 1, v == 0 |
No | Observing ready == 1 creates the synchronization edge, forcing the prior payload write to happen-before the later payload read. |
The forbidden outcome is the one interviewers often want:
The reasoning is a chain, not an intuition about “cache flushing”:
payload = 42is sequenced before the release store toready.- The acquire load reads
1from that release store. - Therefore, the release store synchronizes-with the acquire load.
- The acquire load is sequenced before
v = payload. - Therefore,
payload = 42happens-before the read ofpayload. - The read cannot legally produce the old
0.
This says nothing about a literal instant when all caches suddenly contain 42. It is a guarantee about the execution the program is allowed to observe.
The barrier-direction image below is a useful visual mnemonic. It classifies ordering directions such as LoadLoad and StoreStore. For this lesson, use it conceptually: an acquire constrains accesses after the observation, while a release constrains accesses before publication. The exact instructions emitted differ across ARM, x86, and other targets.

Trace 2: acquire-release is not a global sequential order
Now consider two separate atomic variables, initially zero.
atomic_int x = 0;
atomic_int y = 0;
/* Thread 0 */
atomic_store_explicit(&x, 1, memory_order_release);
int r0 = atomic_load_explicit(&y, memory_order_acquire);
/* Thread 1 */
atomic_store_explicit(&y, 1, memory_order_release);
int r1 = atomic_load_explicit(&x, memory_order_acquire);
The result to analyze is:
This outcome is permitted with acquire-release atomics, including on weakly ordered architectures.
Why?
- Thread 0’s acquire load reads the initial
0fromy, not Thread 1’s release-store of1. - Thread 1’s acquire load reads the initial
0fromx, not Thread 0’s release-store of1. - Therefore, neither acquire observes the other release.
- Without either synchronizes-with edge, there is no cross-thread happens-before relationship linking these operations.
Each atomic object is still coherent: all observers agree on a valid order of modifications to x, and likewise to y. What acquire-release does not impose is a single total order spanning both x and y.
The complete set of result pairs is permitted here:
r0 |
r1 |
Permitted with acquire-release? |
|---|---|---|
| 0 | 0 | Yes |
| 0 | 1 | Yes |
| 1 | 0 | Yes |
| 1 | 1 | Yes |
The surprising row is . Both threads can perform their store locally while their subsequent load still observes the old value of the other location. This is commonly called the store-buffering litmus test.
The same 0, 0 outcome is forbidden if all four operations are sequentially consistent atomics, because sequential consistency adds a single global total order for the atomic operations. It is not forbidden merely because each operation is acquire or release.
This is a key correction to an oversimplified slogan:
Acquire-release is strong enough for directed publication through a chosen synchronization variable. It is not a request that all threads agree on one global timeline for all atomic variables.
The programming-language memory-model reading presents exactly this distinction, beginning with acquire-release as a release-observed-by-acquire relationship and then using the store-buffering test to contrast it with sequential consistency.
Programming Language Memory Models ...
Read the “Acquire/release atomics” subsection for a compact formal explanation of the release-to-acquire happens-before edge, followed by its Store Buffering litmus test.
In “Acquire/release atomics,” begin with the acquire-release definition. Then continue to the “Store Buffering” litmus test and its explanation. Focus on why r1 = 0, r2 = 0 is allowed for acquire-release atomics even though each individual atomic location remains coherent. Contrast that with the total-order requirement for sequentially consistent atomics.
Why a branch is not an acquire operation
This mistake is particularly relevant in firmware mailbox code:
while (flag == 0) {
/* poll */
}
value = mailbox_data;
A source-level branch saying “only read data after flag is nonzero” does not, by itself, force a weakly ordered CPU to treat the data read as ordered after the flag read. The compiler and CPU need an explicit synchronization operation or a genuine dependency that the architecture recognizes.
At the C11 level, express the required operation directly:
while (atomic_load_explicit(&flag, memory_order_acquire) == 0) {
/* poll */
}
value = mailbox_data;
At the Linux-kernel level, the analogous pattern is generally an acquire load of the flag followed by the payload access.
At raw ARM assembly level, ordering may require an appropriate barrier or acquire-load instruction. The following short segment shows the same issue through a polled mailbox: the producer must not expose the flag before the payload, and the consumer must not allow the payload read to move ahead of observing the flag.
Arm's Weakly-Ordered Memory Model and Barrier Requirements - Ash Wilding, Amazon
Watch “Arm's Weakly-Ordered Memory Model and Barrier Requirements” by Ash Wilding on The Xen Project. This segment uses a two-CPU mailbox to make the otherwise abstract reorderings concrete.
Watch the mailbox example. Track the two independent accesses on the producer—payload store and flag store—then the flag load and payload load on the consumer. Focus on why the branch on the flag is not itself a sufficient ordering mechanism for the later mailbox read.
For your Zephyr and Linux driver work, the transfer of ownership is usually more elaborate than a one-bit mailbox: a descriptor index, buffer address, ownership bit, and interrupt or event may all be involved. The same reasoning still applies. Find the object that signals ownership transfer, publish descriptor contents before releasing that object, and acquire it before dereferencing the descriptor.
A compact interview drill framework
When shown a trace, avoid beginning with “ARM can reorder anything” or “acquire-release makes it safe.” Both are too broad. Instead, use this structure:
-
State the initial values.
“xandybegin at zero.” -
Identify every atomic object and memory order.
“xandyare distinct atomic objects; each store is release and each load is acquire.” -
Find reads-from relationships.
“For the proposed0,0result, each acquire reads the initialization, not the other thread’s release.” -
Establish—or reject—synchronization edges.
“Because no acquire reads a release-written value, no synchronizes-with edge exists.” -
Conclude at the right strength.
“The outcome is permitted under acquire-release. It would be excluded by sequential consistency’s global atomic order.”
For a publication trace, the corresponding response is:
“The acquire reads the release-published flag, so the producer’s preceding payload writes happen-before the consumer’s later payload read. A new flag with an old payload is forbidden.”
Avoid two common incorrect claims:
-
“Acquire always sees the newest value.”
No. An acquire constrains what follows if it observes a relevant release. It does not mean “read the latest write across the system.” -
“Release flushes memory globally.”
No. Release establishes an ordering/publishing relationship; it is not a universal cache-management command and does not solve device DMA visibility or MMIO ordering.
Those device-specific questions are deliberately separate; the module’s final lesson will distinguish CPU memory ordering from cache coherency and device-I/O barriers.
Key takeaways
- Analyze traces by determining what each load reads from, not by looking only at the words “acquire” and “release.”
- A release store and an acquire load establish a cross-thread happens-before edge only when the acquire observes that release on the same synchronization object.
- In a proper publication trace, observing the published flag forbids observing an older associated payload.
- Acquire-release provides per-location coherence and directed publication; it does not impose one global order across separate atomic variables.
- Therefore, the store-buffering outcome where both threads read
0is permitted with acquire-release atomics, but not with sequentially consistent atomics. - A control-flow branch on a flag is not a replacement for an acquire operation on weakly ordered hardware.
Next, you will move from analyzing atomic traces to choosing the right synchronization primitive—mutex, spinlock, semaphore, completion/event, or atomic operation—for thread and interrupt-context scenarios.