Fences and Memory Barriers
This chapter covers memory fences (barriers): standalone operations that order memory accesses without themselves accessing an atomic object.
Why This Matters
Fences express ordering constraints that are not tied to a single atomic load or store. They are essential for correct lock-free code, and understanding them explains *why* acquire/release work on weak-memory architectures.
Prerequisites
c.conc.6— memory orders.
Core Concept
A fence (C11: atomic_thread_fence) is a standalone memory-ordering operation:
#include <stdatomic.h>
atomic_thread_fence(memory_order_acquire);
atomic_thread_fence(memory_order_release);
atomic_thread_fence(memory_order_acq_rel);
atomic_thread_fence(memory_order_seq_cst);
Fences order *surrounding* memory operations rather than a single atomic operation. For example, a release fence ensures all prior writes are visible before any subsequent write; an acquire fence ensures subsequent reads happen after all prior reads.
Examples
Release/acquire using fences
atomic_int flag = ATOMIC_VAR_INIT(0);
int data = 0;
/* producer */
data = 42;
atomic_thread_fence(memory_order_release);
atomic_store_explicit(&flag, 1, memory_order_relaxed);
/* consumer */
while (atomic_load_explicit(&flag, memory_order_relaxed) == 0)
;
atomic_thread_fence(memory_order_acquire);
int v = data; /* 42 */
This is the fence-based equivalent of the release/acquire message-passing pattern.
How It Works
A fence constrains the compiler and CPU from reordering memory operations across it. The compiler emits a hardware barrier instruction (mfence/lfence/ sfence on x86, dmb/dsb on ARM, fence on RISC-V) and/or marks its own reordering as forbidden.
Variations
Compiler barrier vs. hardware barrier
A compiler barrier (asm volatile("" ::: "memory")) only stops compiler reordering. A memory barrier/fence also stops CPU reordering. Use the right one for the job.
Fences vs. atomic operations
Atomic operations with acquire/release order are usually preferred because they are easier to reason about. Standalone fences are for cases where the ordering is independent of a specific atomic access.
Common Mistakes
- Using a compiler barrier when a hardware fence is needed (or vice versa).
- Assuming a fence alone makes data atomic (it only orders).
- Over-fencing (unnecessary
seq_cstcosts performance).
Undefined Behavior
- A data race on non-atomic data if the fence protocol is wrong.
Portability
- C11 fences are standard; the underlying instructions are
architecture-specific.
Under the Hood
On x86, loads are acquire-like and stores are release-like by default, so fences are mostly free; seq_cst needs mfence/lock prefixes. On ARM/RISC-V, fences map to explicit dmb/fence instructions.
Practical Usage
- Prefer atomic operations with memory orders; use fences only when necessary.
- Use release/acquire fences for protocols with relaxed atomics.
- Avoid
seq_cstfences unless you truly need global ordering.
Exercises
1. Implement the fence-based message-passing example and verify with TSan. 2. Compare the generated assembly of a fence on x86 vs. ARM. 3. Explain the difference between a compiler barrier and a memory fence.
Deep Challenge
Implement a lock-free flag (release store + relaxed load) using fences, and explain why a plain volatile flag is insufficient. Show the assembly on a weak-memory architecture.
Related Concepts
c.conc.6— memory orders.c.cpu.barrier— compiler/hardware barriers.c.conc.8— lock-free programming.
References
- ISO C §7.17 (atomics/fences); architecture manuals.
Verification
- Fence semantics are standard C11.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] atomic_thread_fence
- [ ] Acquire/release fences
- [ ] Compiler vs. hardware barrier
- [ ] Fence vs. atomic op
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.conc.fence | 0 | 7 |
| c.conc.barrier | 0 | 7 |