但是编译器原则上不应该缓存 volatile 变量,对吗?
不,编译器原则上必须在每次读取/写入变量时读取/写入变量的地址。
[编辑:至少,它必须这样做,直到实现认为该地址的值是“可观察的”。正如 Dietmar 在他的回答中指出的那样,一个实现可能会声明“无法观察到”正常的记忆。对于使用调试器、或其他超出标准范围的东西的人来说,这会让他们感到惊讶mprotect
,但原则上它可以符合。]
在完全不考虑线程的 C++03 中,由实现来定义在线程中运行时“访问地址”的含义。像这样的细节被称为“记忆模型”。例如,Pthreads 允许每个线程缓存整个内存,包括易失性变量。IIRC,MSVC 保证适当大小的 volatile 变量是原子的,并且它将避免缓存(相反,它将刷新到所有内核的单个一致缓存)。它提供这种保证的原因是因为在 Intel 上这样做相当便宜——Windows 只关心基于 Intel 的架构,而 Posix 关心的是更奇特的东西。
C++11 为线程定义了一个内存模型,它说这是一个数据竞争(即volatile
不能确保一个线程中的读取相对于另一个线程中的写入是按顺序排列的)。两个访问可以按特定顺序排序,按未指定顺序排序(标准可能会说“不确定顺序”,我不记得了),或者根本不排序。根本没有排序是不好的——如果两个未排序的访问中的任何一个是写入,那么行为是未定义的。
这里的关键是“我从一个线程修改一个元素,然后读取它的线程没有注意到变化”中隐含的“然后”。您假设操作是按顺序排列的,但事实并非如此。就读取线程而言,除非您使用某种同步,否则其他线程中的写入不一定发生。实际上它比这更糟糕——你可能会从我刚刚写的内容中想到,它只是未指定的操作顺序,但实际上具有数据竞争的程序的行为是未定义的。