# Memory Barriers: What volatile Won't Do For You

> C's volatile keyword is the most misunderstood tool in concurrent programming. It stops the compiler from caching a value — but it does nothing about the reordering that actually breaks your lock-free code.

URL: https://harshith.in/blog/memory-barriers-and-volatile/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: concurrency, systems

Someone marks a shared flag `volatile`, the race goes away on their machine, and
a myth is born: `volatile` makes things thread-safe. It doesn't. It solves a
different problem, and the bug it hides comes back the moment the hardware
reorders your memory accesses.

## What volatile Actually Promises

`volatile` tells the compiler: this value can change behind your back, so don't
cache it in a register and don't optimize away reads or writes. That's it. It was
designed for memory-mapped hardware registers, not threads.

```c
volatile int ready;        // compiler will re-read it every time
while (!ready) { }         // won't be hoisted into "while(true)"
```

This stops *one* class of bug — the compiler assuming a value never changes. It
says nothing about **ordering** or **atomicity**.

## The Reordering You Can't See

Both the compiler and the CPU may reorder independent memory operations for
speed. Consider the classic publish pattern:

```c
data = 42;          // (1) write the payload
ready = 1;          // (2) announce it's ready
```

Even with `ready` volatile, nothing guarantees thread B sees (1) before (2). The
CPU's store buffer can make `ready = 1` globally visible *before* `data = 42`.
The reader then sees `ready` set and reads garbage from `data`:

```text
   writer (CPU 0)            reader (CPU 1)
   store data = 42  ──┐      load ready  → 1
   store ready = 1  ──┘      load data   → ??? (maybe still 0)
        store buffer may reorder these as seen by CPU 1
```

This is not a compiler bug. It's the memory model the hardware is allowed to use.

## What a Barrier Does

A **memory barrier** (fence) constrains that ordering. The two that matter for
the publish pattern:

- **Release** on the writer: everything before it is visible before the flagged
  store.
- **Acquire** on the reader: nothing after it is reordered before the flagged
  load.

Pair them and you get a happens-before relationship across threads:

```c
#include <stdatomic.h>

// writer
data = 42;
atomic_store_explicit(&ready, 1, memory_order_release);

// reader
if (atomic_load_explicit(&ready, memory_order_acquire) == 1)
    use(data);   // guaranteed to see 42
```

The acquire/release pair is the actual fix. It's also usually *cheaper* than a
full barrier, because it only forbids the reorderings that would break you.

## The Rule of Thumb

- Use `volatile` for memory-mapped I/O and signal handlers. Not for threads.
- Use atomics with explicit ordering for cross-thread communication. Default to
  `seq_cst` until you can prove acquire/release is enough.
- If two threads touch the same non-atomic location and one writes, that's a data
  race — undefined behavior, full stop.

## Takeaways

- `volatile` prevents the compiler from caching a value; it does **not** provide
  ordering or atomicity.
- Real bugs come from compiler *and* CPU reordering of independent accesses.
- Acquire/release barriers (via atomics) establish happens-before across threads
  and are the correct, often cheaper, tool.
