Skip to main content

volatile Keyword

volatile tells compiler that a variable can change unexpectedly (hardware, interrupts, other threads). Prevents certain optimizations. Not for thread synchronization - use atomics instead.

Not for Threading

volatile ≠ thread-safe. Use std::atomic for multi-threading. volatile is for hardware/interrupts only.

What volatile Does

volatile int hardware_register;

// Without volatile: compiler might optimize away "redundant" reads
int x = hardware_register;
int y = hardware_register; // Compiler might reuse x (wrong!)

// With volatile: forces actual reads
volatile int hardware_register;
int x = hardware_register; // Read from hardware
int y = hardware_register; // Read again (different value possible)

Guarantee: Every access actually touches memory - no optimization.

volatile Prevents

Optimization Away

// Without volatile
int status;
while (status == 0) {
// Wait
}
// Compiler: "status never changes in loop, optimize to infinite loop"
// while (true) {} ❌

// With volatile
volatile int status;
while (status == 0) {
// Compiler must read status each iteration ✅
}

Read/Write Reordering

volatile int* port = (int*)0x40000000;

*port = 1;
*port = 2;

// Compiler must not:
// - Eliminate first write
// - Reorder writes
// Both writes happen in order ✅

Register Caching

// Without volatile
int sensor_value;
for (int i = 0; i < 1000; ++i) {
if (sensor_value > 100) break;
}
// Compiler caches sensor_value in register ❌

// With volatile
volatile int sensor_value;
for (int i = 0; i < 1000; ++i) {
if (sensor_value > 100) break; // Reads from memory each time ✅
}

Use Cases

1. Memory-Mapped I/O

// Hardware register at fixed address
volatile uint32_t* const UART_STATUS = (uint32_t*)0x40001000;
volatile uint32_t* const UART_DATA = (uint32_t*)0x40001004;

void send_byte(uint8_t byte) {
while (*UART_STATUS & 0x01) { // Wait for ready
// Must read UART_STATUS each iteration
}
*UART_DATA = byte; // Write to hardware
}

2. Interrupt Service Routines (ISR)

volatile bool data_ready = false;

void interrupt_handler() {
// Called by hardware
data_ready = true; // ISR modifies
}

void main_loop() {
while (!data_ready) {
// Wait for interrupt
// Must read data_ready each time
}
process_data();
}

3. Signal Handlers (POSIX)

#include <signal.h>

volatile sig_atomic_t interrupted = 0;

void signal_handler(int sig) {
interrupted = 1; // Signal handler modifies
}

int main() {
signal(SIGINT, signal_handler);

while (!interrupted) {
// Do work
// Must read interrupted each iteration
}
}

What volatile Does NOT Do

Not Thread-Safe

// ❌ WRONG: volatile doesn't make this thread-safe!
volatile int counter = 0;

void thread_func() {
for (int i = 0; i < 1000; ++i) {
counter++; // ❌ RACE CONDITION!
}
}

// ✅ CORRECT: Use atomic
std::atomic<int> counter{0};

void thread_func() {
for (int i = 0; i < 1000; ++i) {
counter++; // ✅ Thread-safe
}
}

No Memory Barriers

// ❌ volatile doesn't prevent reordering across variables
volatile int flag;
int data;

void producer() {
data = 42; // These can be reordered!
flag = 1;
}

// ✅ Atomic provides proper synchronization
std::atomic<int> flag;
int data;

void producer() {
data = 42;
flag.store(1, std::memory_order_release); // ✅ Proper sync
}

No Atomicity Guarantee

// ❌ Not atomic on all platforms
volatile long long value;
value = 0x123456789ABCDEF0; // May be two separate writes!

// ✅ Guaranteed atomic
std::atomic<long long> value;
value = 0x123456789ABCDEF0; // Atomic write

volatile vs atomic vs mutex

NeedUse
Hardware registersvolatile
ISR to main communicationvolatile + sig_atomic_t
Multi-threadingstd::atomic
Complex critical sectionsstd::mutex
// Hardware I/O
volatile uint32_t* hardware = (uint32_t*)0x40000000;

// Thread communication
std::atomic<bool> ready{false};

// Complex data structure
std::mutex mtx;
std::vector<int> shared_data;

volatile Pointer vs Pointer to volatile

// Pointer to volatile data
volatile int* ptr;
// *ptr is volatile, ptr is not
// ptr can be cached, *ptr cannot

// Volatile pointer
int* volatile ptr;
// ptr is volatile, *ptr is not
// *ptr can be cached, ptr cannot

// Both volatile
volatile int* volatile ptr;
// Neither can be cached

// Common usage
volatile uint32_t* const hw_register = (uint32_t*)0x40000000;
// hw_register address is constant
// *hw_register is volatile

Real-World Example: Embedded System

// Hardware registers
struct UART {
volatile uint32_t STATUS;
volatile uint32_t DATA;
volatile uint32_t CONTROL;
};

volatile UART* const uart = (UART*)0x40001000;

void uart_send(const char* str) {
while (*str) {
// Wait for transmit ready
while (!(uart->STATUS & 0x01)) {
// Must read STATUS each time
}

// Send character
uart->DATA = *str++;
}
}

void uart_receive(char* buffer, size_t size) {
for (size_t i = 0; i < size; ++i) {
// Wait for receive ready
while (!(uart->STATUS & 0x02)) {
// Must read STATUS each time
}

// Read character
buffer[i] = uart->DATA;
}
}

C++ Memory Model Position

// C++ standard: volatile has no thread synchronization semantics
// Java/C#: volatile has memory barrier semantics (different!)

// ❌ C++: Don't use for threading
volatile bool flag;

// ✅ C++: Use atomic for threading
std::atomic<bool> flag;

// ✅ volatile only for hardware
volatile int* hardware_register;

Summary

Volatile

volatile prevents compiler from optimizing away memory accesses - forces actual reads/writes every time.

  • Memory aid: "Hardware Only, Not Threads"
  • Use for: Hardware registers, ISRs, signal handlers
  • Don't use for: Threading (use std::atomic)
  • Prevents: Read/write optimization, caching, reordering
  • Doesn't provide: Atomicity, memory barriers, thread safety
// Interview answer:
// "volatile prevents compiler optimizations on variable access -
// forces actual memory read/write every time. Use for hardware
// registers, ISRs, signal handlers where value can change
// unexpectedly outside normal program flow. NOT for threading -
// use std::atomic instead. volatile doesn't provide atomicity
// or memory barriers. Mainly for embedded/systems programming,
// rarely used in application code."