C - Volatile Keyword in C

The volatile keyword in C is a type qualifier that tells the compiler that a variable's value may change unexpectedly, even when the program itself does not appear to modify it. Because of this possibility, the compiler should not assume that the value remains unchanged between accesses and should read the value from memory whenever it is required.

Normally, compilers optimize programs to make them faster. If the compiler sees that a variable has not been modified between two statements, it may reuse the value it previously loaded instead of reading the variable again from memory. This optimization is usually safe for ordinary variables, but it can cause incorrect behavior when a variable can be changed by something outside the normal program flow.

Syntax

The basic syntax for declaring a volatile variable is:

volatile int value;

Here, volatile is the qualifier and int is the data type.

It can also be used with other data types:

volatile char status;
volatile float temperature;
volatile unsigned int counter;

The important point is that volatile does not create a new data type. It modifies how the compiler should treat accesses to the variable.

Why Is volatile Needed?

Consider the following example:

int flag = 0;

while (flag == 0) {
    // Wait
}

Suppose flag can be changed by an external event, such as hardware or an interrupt. The compiler might optimize the loop by assuming that flag cannot change because there is no statement inside the loop that modifies it.

This could result in behavior similar to continuously checking a previously loaded value instead of reading flag again.

To indicate that flag may change unexpectedly, it can be declared as:

volatile int flag = 0;

while (flag == 0) {
    // Wait
}

The volatile qualifier tells the compiler that every access to flag is significant and that it should not simply assume that the value remains unchanged.

Common Uses of volatile

The volatile keyword is particularly useful in systems programming, embedded programming, device drivers, and programs that interact with hardware.

1. Hardware Registers

A common use of volatile is accessing hardware registers.

For example:

volatile unsigned int *status_register;

status_register = (volatile unsigned int *)0x40000000;

while (*status_register == 0) {
    // Wait for hardware
}

The memory location represented by status_register may correspond to a hardware register. The hardware can change its value independently of the program.

Without volatile, the compiler might make assumptions about the value and optimize repeated accesses.

With volatile, the compiler knows that the value may change outside the normal program execution.

2. Interrupt Service Routines

In embedded systems, an interrupt service routine may modify a variable while the main program is running.

For example:

volatile int interrupt_flag = 0;

void interrupt_handler(void)
{
    interrupt_flag = 1;
}

int main(void)
{
    while (interrupt_flag == 0) {
        // Wait for interrupt
    }

    return 0;
}

Here, interrupt_flag can be changed by the interrupt handler.

Declaring it as volatile communicates this behavior to the compiler.

3. Shared Variables Modified Externally

A variable can sometimes be modified by another part of a system that the compiler cannot see or analyze normally.

For example:

volatile int ready = 0;

while (!ready) {
    // Wait until ready
}

The value of ready may be changed by hardware, an interrupt, or another mechanism outside the ordinary flow represented by the current code.

What Does volatile Actually Do?

The most important purpose of volatile is to prevent the compiler from making certain assumptions about accesses to the variable.

For example:

volatile int count;

count = 10;
printf("%d\n", count);
printf("%d\n", count);

The compiler must treat accesses to count as observable operations according to the rules of the C language. It cannot simply assume that the value will always remain unchanged between accesses because something outside the ordinary execution may modify it.

However, volatile does not mean that the variable can never change. In fact, it communicates the opposite: the value may change in ways that the compiler cannot predict.

volatile Does Not Make Code Thread-Safe

One common misconception is that volatile makes a variable safe to use between multiple threads.

It does not.

For example:

volatile int counter = 0;

Using volatile does not automatically make operations such as:

counter++;

safe when multiple threads access counter.

The operation:

counter++;

involves reading the value, modifying it, and writing the result back. Multiple threads can interfere with these operations.

For multithreaded programs, synchronization mechanisms such as mutexes, atomic operations, or other appropriate concurrency mechanisms should be used.

For example, C11 provides atomic types through <stdatomic.h>:

#include <stdatomic.h>

atomic_int counter;

Therefore, volatile and atomic operations serve different purposes.

Difference Between volatile and Normal Variables

Consider:

int status;

The compiler generally assumes that the value of status changes only through operations that it can account for within the program.

Now consider:

volatile int status;

The compiler must take into account that the value may be changed by something outside the ordinary code it is analyzing.

The difference is therefore primarily about compiler assumptions and optimization, not about the amount of memory occupied by the variable.

volatile with Pointers

The volatile qualifier can also be used with pointers.

For example:

volatile int *ptr;

This means that the data being accessed through ptr is volatile.

For example:

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

int value = *ptr;

The compiler should treat the access through ptr as a volatile access.

There are also different combinations involving the pointer itself:

int *volatile ptr;

Here, the pointer itself is volatile, while the integer object it points to is not necessarily volatile.

Another combination is:

volatile int *volatile ptr;

Here, both the pointer and the object it points to are volatile.

Understanding this distinction is important in low-level C programming.

volatile with const

volatile can also be combined with const.

For example:

const volatile int status;

This means that the program should not modify status through this particular declaration, but its value can still change externally.

This can be useful for read-only hardware registers.

For example:

const volatile unsigned int status_register;

The program can read the register, but should not attempt to modify it through the variable.

At the same time, hardware may change its value.

Important Characteristics of volatile

The volatile qualifier has several important characteristics:

  1. It tells the compiler that the value may change unexpectedly.

  2. It prevents certain optimizations involving accesses to the volatile object.

  3. It is commonly used with hardware registers.

  4. It is useful for variables modified by interrupt handlers.

  5. It does not guarantee atomic operations.

  6. It does not provide thread synchronization.

  7. It does not automatically make a program thread-safe.

  8. It does not guarantee memory ordering between threads.

  9. It does not mean that every machine-level access is necessarily indivisible.

  10. It is primarily concerned with how the compiler treats accesses to the object.

Example Program

#include <stdio.h>

volatile int flag = 0;

void set_flag(void)
{
    flag = 1;
}

int main(void)
{
    printf("Initial flag: %d\n", flag);

    set_flag();

    printf("Updated flag: %d\n", flag);

    return 0;
}

In this simple example, the function set_flag() changes flag. In real embedded applications, the change could instead come from an interrupt or hardware event.

The declaration:

volatile int flag = 0;

indicates that the compiler should not treat flag as an ordinary variable whose value can be assumed to remain unchanged between accesses.

When Should volatile Be Used?

volatile should be used when a program interacts with something that can modify a value independently of the normal execution flow.

Typical situations include:

  • Memory-mapped hardware registers

  • Embedded-system status registers

  • Interrupt service routines

  • Hardware devices

  • Certain low-level operating-system components

  • Variables whose values can be modified by external mechanisms

It should not be added to variables simply because the programmer wants to "make them safer" or "prevent bugs." Unnecessary use of volatile can prevent useful compiler optimizations without solving the underlying problem.

Conclusion

The volatile keyword is an important feature of C for low-level and systems programming. It tells the compiler that the value of a variable can change unexpectedly and that accesses to it should therefore be treated carefully during optimization.

Its most common applications are hardware registers, interrupt-related variables, and other situations where the value can change outside the normal sequence of instructions visible to the compiler.

However, volatile should not be confused with synchronization or atomicity. It does not make operations thread-safe and should not be used as a replacement for atomic operations, mutexes, or other concurrency mechanisms. Its primary purpose is to control compiler assumptions about accesses to an object.