volatile关键字
volatile 关键字
volatile 是 C++ 中一个关键的类型修饰符,用于提示编译器不要对被修饰的变量进行优化,因为这个变量可能会被以编译器看不到的方式修改(比如:硬件、中断服务程序、其他线程等)。
volatile int x;
意思是 x 的值可能在程序的控制之外被改变,所以每次访问它都需要从内存重新读取。
本质作用
- 阻止编译器优化读写(比如缓存寄存器、死代码删除、合并写入等)
- 强制每次访问都从内存中读取 / 写入
它完全不能做的事情:
- 不保证多线程下的原子性
- 不保证内存可见性
- 不禁止指令重排
- 不保证线程安全
编译器优化
编译器为了让程序更快,会做很多优化,比如:
- 变量值缓存(避免频繁访问内存)
- 指令重排
- 删除“看起来没必要”的代码
这些优化有时会导致代码行为不符合你写的时候的直觉。特别是当变量的值是被其他线程、硬件、中断修改时,就必须阻止这种优化——这时候就需要 volatile。
变量值被缓存
bool stop = false;
void loop() {
while (!stop) {
// do something
}
}
编译器可能这样优化:
bool stop = false;
void loop() {
if (!stop) {
while (true) {
// do something
}
}
}
编译器认为:
stop没有在loop()中被修改;- 没有看到其他地方改它(比如函数参数或者赋值);
- 所以它大胆推断:
stop在整个函数里一直是false,于是优化成了死循环。
如果改为 volatile:
volatile bool stop = false;
void loop() {
while (!stop) {
// 每次都从内存重新读 stop
}
}
这样编译器就不敢优化,每次都会去内存重新读取 stop 的值,以防被外部修改(例如另一个线程或硬件设备)。
死代码被优化掉
bool ready = false;
void waitReady() {
while (!ready);
printf("Ready!\n");
}
如果 ready 永远没有在这个函数里被修改,且不是 volatile,那么编译器会直接优化掉这个循环——它认为这段代码永远不可能跳出循环(或者干脆删掉整个循环),结果就是 printf 永远不会执行。
指令重排问题的经典例子(双线程同步)
编译器或 CPU 出于性能考虑,可能调整指令的执行顺序,只要单线程看起来执行结果一致,它就会做这样的优化。但在多线程程序中,这种“看起来一样”的优化,可能会导致观察到的执行顺序不一致,从而出现问题。
int a = 0;
int b = 0;
int x, y;
void thread1() {
a = 1;
x = b;
}
void thread2() {
b = 1;
y = a;
}
这两个线程并发运行,理论上我们希望:
x == 1(thread1 看到 thread2 设置了b)y == 1(thread2 看到 thread1 设置了a)
但由于指令重排,有可能出现这样一种“意想不到”的执行顺序:
Thread1 重排后执行顺序:x = b; a = 1;
Thread2 重排后执行顺序:y = a; b = 1;
这会导致最终结果是:
x == 0 && y == 0 // 两个线程都没看到对方的写入
- 编译器或者 CPU 认为
a = 1和x = b无依赖,顺序可以互换; - 同理
b = 1和y = a也可互换;
用 volatile 抑制重排(在某些平台有效)
在 Java 中,volatile 明确禁止读写重排序。
在 C++ 中,volatile 并不能完全禁止指令重排,但是它确实对部分编译器(如 GCC)会:
- 禁止将访问
volatile的语句移动到一起 - 禁止访问顺序乱序执行
volatile int a = 0;
volatile int b = 0;
a = 1;
b = 2;
在没有 volatile 的情况下,可能变成:
b = 2;
a = 1;
但加了 volatile 后,编译器必须按照顺序生成写入指令。
多线程下的解决方案
C++11 引入了 std::atomic 和内存序(memory_order)模型,来真正解决这个问题。
std::atomic<int> a{0}, b{0};
用 memory_order_seq_cst(默认),可以确保跨线程的执行顺序与代码顺序一致,防止乱序。
搭配 const 使用
volatile const int x = 5;
表示值不能由程序修改(const),但可能被外部修改(volatile)。
“外部”指的是编译器看不见、不是通过当前 C++ 代码修改的地方。常见的“外部”有这些几种情况:
硬件设备
比如在嵌入式程序里读取一个温度传感器的值,它会被硬件定时更新:
const volatile int* TEMP_SENSOR = (int*)0xFF00; // 硬件地址
int readTemperature() {
return *TEMP_SENSOR; // 每次都从硬件读取
}
const:代码不能写*TEMP_SENSOR = 5;,因为不该去写传感器的值。volatile:但这个值会被硬件更新,所以每次都要重新读取,不能优化成常量。
中断服务程序(ISR)
中断可能在代码之外发生,并修改变量。
volatile const int counter;
void ISR() {
// 中断服务程序里修改 counter
*(int*)&counter = 42; // 非常规方式修改
}
虽然代码里标记它为 const 不可改,但中断还是可能通过“技巧”或者底层方式改写它的值。
其他线程
在多线程程序中,一个线程可能在写,另一个线程只读。
volatile const int flag;
void threadA() {
// 不能写 flag,读它的值
while (flag == 0) { /* wait */ }
}
另一个线程偷偷通过类型转换写入(不推荐这样写):
void threadB() {
*(int*)&flag = 1;
}
- 虽然在
threadA里flag是const,不能写; - 但
threadB通过强转指针绕开了这个限制。
不建议用 volatile 做线程同步,应使用 std::atomic。
volatile 和其他机制的对比
| 场景 | 推荐方式 | volatile 是否适合 |
|---|---|---|
| 硬件寄存器访问 | volatile |
是 |
| 中断标志 | volatile |
是 |
| 多线程控制标志(仅读写) | std::atomic<bool> |
可选,但推荐用 atomic |
| 多线程数据共享/同步 | std::atomic / mutex |
否 |
| 实现锁、CAS 等并发结构 | std::atomic |
否 |

浙公网安备 33010602011771号