volatile 是什么?
volatile 是一个类型修饰符(Type Modifier),就像 const 一样,它告诉编译器:“不要对这个变量进行任何形式的优化,每次使用它时,都必须从它所在的内存地址中重新读取它的值。”

volatile 的核心作用是抑制编译器的优化,确保变量的可见性和顺序性。
为什么需要 volatile?—— 编译器优化的“副作用”
为了理解 volatile 的必要性,我们首先要明白编译器会做什么,编译器的目标之一是生成高效的机器码,它会进行各种优化,
- 代码移动:将循环中不变的变量读取操作移到循环外面。
- 寄存器缓存:将频繁使用的变量值缓存在 CPU 寄存器中,以减少内存访问次数。
- 死代码消除:移除那些看起来永远不会被执行或结果不会被使用的代码。
这些优化在单线程的、可预测的程序中是极好的,但在某些特殊场景下,这些优化会破坏程序的正确性。
一个经典的例子:硬件寄存器

假设你正在编写一个嵌入式程序,需要控制一个 LED 灯,这个 LED 灯的状态由一个内存映射的硬件寄存器控制,地址是 0x12345678。
#define LED_CTRL_REG (*(volatile unsigned int *)0x12345678)
void turn_on_led() {
LED_CTRL_REG = 1; // 设置 LED 为开
}
如果没有 volatile,编译器会看到这段代码:
- 读取
LED_CTRL_REG的值(从地址0x12345678)。 - 将值
1写入LED_CTRL_REG(到地址0x12345678)。
编译器可能会进行如下“优化”:
- 读取优化:编译器发现你只是要写入
1,它才不关心你写入之前LED_CTRL_REG里是什么鬼东西呢!它可能会完全省略掉第一次的读取操作,直接生成一条mov指令,把1写入那个地址,这在大多数情况下是没问题的。 - 更极端的优化:如果编译器发现
turn_on_led函数被调用后,LED_CTRL_REG的值再也没有被读过,它可能会认为LED_CTRL_REG = 1;这条语句是“无用的”,因为它没有产生任何“可观察到的效果”(从程序的角度看,内存被改了,但程序本身没读),于是直接删除整条语句!这会导致 LED 灯永远不会亮。
加上 volatile 后,LED_CTRL_REG 就变成了一个易变变量,编译器会收到指令:“这个家伙的值随时可能被我不知道的东西(比如硬件本身)改变,你不能相信你缓存在寄存器里的旧值。”

编译器会:
- 每次访问
LED_CTRL_REG,都生成一条从0x12345678读取的指令。 - 每次赋值给
LED_CTRL_REG,都生成一条写入0x12345678的指令。 - 绝不会进行任何可能导致操作被省略或移动的优化。
另一个例子:多线程共享变量
假设有两个线程,一个线程(生产者)更新一个全局变量,另一个线程(消费者)读取它。
int global_flag = 0; // 共享标志
// 线程1 (生产者)
void producer_thread() {
// ...做一些耗时的工作...
global_flag = 1; // 工作完成,设置标志
}
// 线程2 (消费者)
void consumer_thread() {
while (global_flag == 0) {
// 等待...
}
// ...处理工作...
}
在没有同步机制(如互斥锁)的情况下,consumer_thread 中的 while 循环可能会被编译器优化成死循环。
-
编译器优化:编译器分析
consumer_thread的代码,发现global_flag在循环内部没有被修改过,它可能会把global_flag == 0的结果计算出来,发现是true,然后直接生成一个无限循环的汇编指令(jmp),因为它认为global_flag的值永远不会在循环内部改变。 -
使用
volatile:如果我们将global_flag声明为volatile:volatile int global_flag = 0;
编译器就知道
global_flag的值可能会在“看不见”的地方被改变(比如另一个线程),它不会把while (global_flag == 0)优化成死循环,每次循环迭代,它都会从内存中重新读取global_flag的值,从而能够正确地检测到生产者线程设置的1并退出循环。
注意:volatile 不能保证原子性,在上面的多线程例子中,global_flag = 1; 和 global_flag == 0; 的读取/写入操作可能不是原子的,如果两个线程同时读写,仍然可能发生数据竞争。volatile 只解决了“总是从内存读”和“保证写入内存”的问题,但没有解决“操作不被打断”的问题,对于多线程同步,通常需要更强大的工具,如 stdatomic.h (C11) 或互斥锁。
volatile 的核心作用总结
-
保证可见性
- 当一个线程修改了一个
volatile变量时,新值会立刻同步到主内存。 - 当另一个线程读取一个
volatile变量时,会直接从主内存读取,而不是从自己的工作内存(或CPU缓存/寄存器)中读取。 - 这确保了所有线程都能“看到”这个变量的最新值。
- 当一个线程修改了一个
-
防止编译器优化
- 禁止指令重排序:编译器不会随意地重新安排
volatile变量的读写顺序。 - 禁止死代码消除:所有对
volatile变量的读写操作都会被保留,即使编译器认为它们是“无用的”。 - 禁止寄存器缓存:每次访问
volatile变量,都必须从内存中读取,或写入内存,不能长期缓存在寄存器里。
- 禁止指令重排序:编译器不会随意地重新安排
volatile 的典型使用场景
你应该在以下情况考虑使用 volatile:
-
内存映射的硬件寄存器
控制寄存器、状态寄存器、数据端口等,硬件可以在任何时候改变它们,程序也需要随时读取它们的状态。
-
中断服务程序 中共享的变量
-
当一个普通线程/函数和中断服务程序共享一个全局变量时,这个变量必须声明为
volatile。 -
示例:
volatile int g_interrupt_flag = 0; void main_loop() { while (1) { if (g_interrupt_flag) { // 处理中断 g_interrupt_flag = 0; } // ...其他任务... } } void ISR_Handler() { // 中断服务程序 // ...硬件中断发生... g_interrupt_flag = 1; // 设置标志 }如果没有
volatile,main_loop中的if (g_interrupt_flag)可能会被优化掉,导致永远无法检测到中断。
-
-
多线程/多任务环境中共享的非原子变量
- 当多个线程共享一个变量,且这个变量的读写操作本身是原子的(比如一个
int在大多数系统上就是原子的),但需要确保可见性时,可以使用volatile。 - 重要提醒:这仅适用于简单的标志位,如果涉及“读-改-写”操作(如
i++),volatile是不够的,因为i++不是原子操作,这时应该使用原子类型(stdatomic.h)。
- 当多个线程共享一个变量,且这个变量的读写操作本身是原子的(比如一个
-
全局变量,其地址可能被
setjmp或longjmp使用- 这是一种比较少见的情况,
setjmp/longjmp会绕过正常的函数调用栈,导致栈上的变量状态不一致。volatile可以帮助在这种情况下保持某些变量的正确性。
- 这是一种比较少见的情况,
volatile 的常见误区
-
误区:
volatile可以保证原子性。- 错误。
volatile不能保证复合操作的原子性。 - 例子:
volatile int counter = 0; counter++;这个操作不是原子的,它包含“读取-修改-写入”三个步骤,在多线程环境下,如果两个线程同时执行counter++,可能会导致结果不正确(丢失一次更新),要保证原子性,应使用原子操作。
- 错误。
-
误区:
volatile可以替代锁,用于多线程同步。- 错误。
volatile的功能非常有限,它只保证了可见性和一定的顺序性,但没有提供互斥访问的保证,对于复杂的共享数据结构,必须使用互斥锁、信号量等同步原语。
- 错误。
-
误区:
volatile能让变量变得线程安全。- 错误,线程安全是一个更广泛的概念,它不仅要求可见性,还要求原子性和同步。
volatile只是线程安全工具箱中的一件小工具,而不是万能药。
- 错误,线程安全是一个更广泛的概念,它不仅要求可见性,还要求原子性和同步。
volatile 与 const 的结合使用
volatile 和 const 可以同时修饰一个变量,这在嵌入式开发中很常见。
// 一个指向只读硬件寄存器的指针,这个寄存器的值可能随时被硬件改变 const volatile int * const hardware_reg = (const volatile int *)0x12345678;
我们来分解这个声明:
- 最右边的
const:hardware_reg是一个指针常量,意味着它本身的值(即它指向的地址0x12345678)不能被修改,你不能让它指向别的地方。 - 中间的
volatile:它指向的内存地址(0x12345678是易变的,可能被硬件改变,所以每次读取都必须从内存中读。 - 最左边的
const:它指向的int是只读的,你试图通过*hardware_reg = some_value;来修改这个地址的值是不允许的,编译器会报错。
这个声明完美地描述了一个“只读的、由硬件自动更新的”内存映射寄存器。
| 特性 | 描述 |
|---|---|
| 核心作用 | 抑制编译器优化,确保变量的可见性和顺序性。 |
| 关键点 | 每次访问都从内存读取,每次写入都立即写入内存。 |
| 主要用途 | 硬件寄存器 中断服务程序共享变量 多线程中的简单标志位 |
| 不能做什么 | 不能保证原子性(i++ 不安全)不能替代锁(不提供互斥) 不能保证线程安全 |
与 const |
可以结合使用,如 const volatile int *,表示指向只读、易变内存的指针。 |
volatile 的黄金法则:当一个变量的值可以被程序自身之外的实体(如硬件、另一个线程、操作系统)改变时,就应该将其声明为 volatile。
