Why C/C++ volatile Is Not Atomic in Multithreading
Zusammenfassung
Pixel shares a DEV Community article explaining why volatile does not make shared C/C++ variables safe for concurrent access. It demonstrates how counter++ can lose updates because it involves separate read, increment, and write steps, and clarifies volatile's role in preventing certain compiler optimizations rather than providing atomicity or memory barriers.
DEV Community
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!
}
}
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
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
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 For thread-safe operations in C and C++, never use volatile. Use the standard atomic types: #include
std::atomic counter{0};
void worker() {
for (int i = 0; i < 100000; i++) {
counter.fetch_add(1, std::memory_order_relaxed); // Hardware Atomic!
}
}
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.

Powered by Readybot.io