文章

volatile关键字

volatile告诉编译器变量可能被外部修改,禁止优化,确保每次访问真实读取。

volatile关键字

volatile 关键字

volatile 是 C++ 中一个关键的类型修饰符,用于提示编译器不要对被修饰的变量进行优化,因为这个变量可能会被以编译器看不到的方式修改(比如:硬件、中断服务程序、其他线程等)。

1
volatile int x;

意思是 x 的值可能在程序的控制之外被改变,所以每次访问它都需要从内存重新读取。

本质作用

  1. 阻止编译器优化读写(比如缓存寄存器、死代码删除、合并写入等)
  2. 强制每次访问都从内存中读取 / 写入

完全不能做的事情

  • 不保证多线程下的原子性
  • 不保证内存可见性
  • 不禁止指令重排
  • 不保证线程安全

编译器优化

编译器为了让程序更快,会做很多优化,比如:

  • 变量值缓存(避免频繁访问内存)
  • 指令重排
  • 删除“看起来没必要”的代码

这些优化有时会导致代码行为不符合你写的时候的直觉。特别是当变量的值是被其他线程、硬件、中断修改时,就必须阻止这种优化——这时候就需要 volatile

变量值被缓存

1
2
3
4
5
6
7
bool stop = false;

void loop() {
    while (!stop) {
        // do something
    }
}

编译器可能这样优化:

1
2
3
4
5
6
7
8
9
bool stop = false;

void loop() {
    if (!stop) {
        while (true) {
            // do something
        }
    }
}

编译器认为:

  • stop 没有在 loop() 中被修改;
  • 没有看到其他地方改它(比如函数参数或者赋值);
  • 所以它大胆推断:stop 在整个函数里一直是 false,于是优化成了死循环。

如果改为 volatile

1
2
3
4
5
6
7
volatile bool stop = false;

void loop() {
    while (!stop) {
        // 每次都从内存重新读 stop
    }
}

这样编译器就不敢优化,每次都会去内存重新读取 stop 的值,以防被外部修改(例如另一个线程或硬件设备)。

死代码被优化掉

1
2
3
4
5
6
bool ready = false;

void waitReady() {
    while (!ready);
    printf("Ready!\n");
}

如果 ready 永远没有在这个函数里被修改,且不是 volatile,那么编译器会直接优化掉这个循环——它认为这段代码永远不可能跳出循环(或者干脆删掉整个循环),结果就是 printf 永远不会执行。

指令重排问题的经典例子(双线程同步)

编译器或 CPU 出于性能考虑,可能调整指令的执行顺序,只要单线程看起来执行结果一致,它就会做这样的优化。但在多线程程序中,这种“看起来一样”的优化,可能会导致观察到的执行顺序不一致,从而出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

但由于指令重排,有可能出现这样一种“意想不到”的执行顺序:

1
2
Thread1 重排后执行顺序:x = b; a = 1;
Thread2 重排后执行顺序:y = a; b = 1;

这会导致最终结果是:

1
x == 0 && y == 0  // 两个线程都没看到对方的写入
  • 编译器或者 CPU 认为 a = 1x = b 无依赖,顺序可以互换;
  • 同理 b = 1y = a 也可互换;
用 volatile 抑制重排(在某些平台有效)

在 Java 中,volatile 明确禁止读写重排序。

在 C++ 中,volatile 并不能完全禁止指令重排,但是它确实对部分编译器(如 GCC)会:

  • 禁止将访问 volatile 的语句移动到一起
  • 禁止访问顺序乱序执行
1
2
3
4
volatile int a = 0;
volatile int b = 0;
a = 1;
b = 2;

在没有 volatile 的情况下,可能变成:

1
2
b = 2;
a = 1;

但加了 volatile 后,编译器必须按照顺序生成写入指令。

多线程下的解决方案

C++11 引入了 std::atomic 和内存序(memory_order)模型,来真正解决这个问题。

1
std::atomic<int> a{0}, b{0};

memory_order_seq_cst(默认),可以确保跨线程的执行顺序与代码顺序一致,防止乱序。

搭配 const 使用

1
volatile const int x = 5;

表示值不能由程序修改(const),但可能被外部修改(volatile)。

“外部”指的是编译器看不见、不是通过当前 C++ 代码修改的地方。常见的“外部”有这些几种情况:

硬件设备

比如在嵌入式程序里读取一个温度传感器的值,它会被硬件定时更新:

1
2
3
4
5
const volatile int* TEMP_SENSOR = (int*)0xFF00;  // 硬件地址

int readTemperature() {
    return *TEMP_SENSOR; // 每次都从硬件读取
}
  • const:代码不能写 *TEMP_SENSOR = 5;,因为不该去写传感器的值。
  • volatile:但这个值会被硬件更新,所以每次都要重新读取,不能优化成常量。
中断服务程序(ISR)

中断可能在代码之外发生,并修改变量。

1
2
3
4
5
6
volatile const int counter;

void ISR() {
    // 中断服务程序里修改 counter
    *(int*)&counter = 42;  // 非常规方式修改
}

虽然代码里标记它为 const 不可改,但中断还是可能通过“技巧”或者底层方式改写它的值。

其他线程

在多线程程序中,一个线程可能在写,另一个线程只读。

1
2
3
4
5
6
volatile const int flag;

void threadA() {
    // 不能写 flag,读它的值
    while (flag == 0) { /* wait */ }
}

另一个线程偷偷通过类型转换写入(不推荐这样写):

1
2
3
void threadB() {
    *(int*)&flag = 1;
}
  • 虽然在 threadAflagconst,不能写;
  • threadB 通过强转指针绕开了这个限制。

不建议用 volatile 做线程同步,应使用 std::atomic

volatile 和其他机制的对比

场景推荐方式volatile 是否适合
硬件寄存器访问volatile
中断标志volatile
多线程控制标志(仅读写)std::atomic<bool>可选,但推荐用 atomic
多线程数据共享/同步std::atomic / mutex
实现锁、CAS 等并发结构std::atomic
本文由作者按照 CC BY 4.0 进行授权