DEV Community

Cover image for The Most Dangerous Keyword in C/C++: Why volatile Is Not Atomic
Binary Dose
Binary Dose

Posted on Originally published at binarydose.in

The Most Dangerous Keyword in C/C++: Why volatile Is Not Atomic

The Most Dangerous Keyword in C/C++: Why volatile Is Not Atomic

One of the most persistent myths in systems programming is the belief that declaring a shared variable as volatile makes it safe for concurrent multi-threaded access.

Consider the following multi-threaded counter:

volatile int counter = 0;

void worker() {
    for (int i = 0; i < 100000; i++) {
        counter++; // DATA RACE & LOST UPDATES!
    }
}
Enter fullscreen mode Exit fullscreen mode

Running two threads executing worker() concurrently will almost never result in 200,000. Increments will be silently lost.

To understand why, we must examine the difference between Compiler Optimizations and CPU Hardware Coherence.


1. What volatile Actually Does

The volatile keyword was introduced into the C standard in 1989 for a single specific purpose: interacting with Memory-Mapped I/O (MMIO) hardware registers and asynchronous signal handlers.

It communicates one instruction to the compiler:

"Do not cache this variable inside a CPU register. Do not optimize away repeated reads or writes. Every read must emit a fresh load from memory."

// Without volatile:
while (!flag) {} // Compiler may optimize to: if (!flag) while(1){}

// With volatile:
while (!flag) {} // Compiler emits a CMP instruction reading RAM every loop iteration
Enter fullscreen mode Exit fullscreen mode

2. What volatile Fails to Provide

Failure 1: Zero Atomicity

The operation counter++ is not a single atomic hardware instruction. It decomposes into a 3-step sequence in assembly:

mov eax, dword ptr [counter] ; 1. Read from memory into register
add eax, 1                   ; 2. Increment register value
mov dword ptr [counter], eax ; 3. Write back to memory
Enter fullscreen mode Exit fullscreen mode

If Thread 1 reads counter = 5, and is preempted before Step 3, Thread 2 can read 5, increment to 6, and write back. When Thread 1 resumes, it writes 6, completely obliterating Thread 2's increment.

Failure 2: Zero Memory Barriers

Modern superscalar CPUs (x86, ARM64) execute instructions out of order to maximize execution pipeline throughput.

volatile imposes constraints on the compiler, but it emits zero hardware memory barrier instructions (like MFENCE or DMB). The CPU is free to reorder reads and writes around volatile variables across cache lines.


3. The Modern Solution: std::atomic<T>

For thread-safe operations in C and C++, never use volatile. Use the standard atomic types:

#include <atomic>

std::atomic<int> counter{0};

void worker() {
    for (int i = 0; i < 100000; i++) {
        counter.fetch_add(1, std::memory_order_relaxed); // Hardware Atomic!
    }
}
Enter fullscreen mode Exit fullscreen mode

On x86, this compiles to the hardware instruction lock inc [counter], asserting the CPU bus lock and guaranteeing atomic execution.

Read the full systems engineering breakdown at Binary Dose.

Top comments (0)