编译器有时可以缓存声明为volatile的变量

据我所知,编译器从不优化声明为volatile的变量。 但是,我有一个像这样声明的数组。

 volatile long array[8]; 

不同的线程读写它。 数组的元素仅由其中一个线程修改,并由任何其他线程读取。 但是,在某些情况下,我注意到即使我从一个线程修改一个元素,读取它的线程也不会注意到这个变化。 它继续读取相同的旧值,就好像编译器已将其缓存在某处。 但是编译器本身不应该缓存一个volatile变量,对吗? 那怎么会发生这种情况。

注意 :我没有使用volatile进行线程同步,所以请停止给我答案,比如使用锁或primefaces变量。 我知道volatile,atomic变量和互斥量之间的区别。 另请注意,该体系结构是x86,具有主动缓存一致性。 另外,在我认为变量被其他线程修改之后,我已经读了足够长的时间。 即使经过很长一段时间,阅读线程也看不到修改后的值。

但是编译器本身不应该缓存一个volatile变量,对吗?

不,编译器原则上必须在每次读/写变量时读/写变量的地址。

[编辑:至少,它必须这样做,直到实现认为该地址的值是“可观察的”为止。 正如Dietmar在他的回答中指出的那样,一个实现可能会宣称正常的记忆“无法被观察到”。 对于使用调试器, mprotect或其他超出标准范围的东西的人mprotect ,这会让人感到惊讶,但它原则上可以符合。

在完全不考虑线程的C ++ 03中,由实现来定义在线程中运行时“访问地址”的含义。 像这样的细节被称为“记忆模型”。 例如,Pthreads允许每个线程缓存整个内存,包括volatile变量。 IIRC,MSVC保证适当大小的volatile变量是primefaces的,并且它将避免缓存(相反,它将刷新到所有内核的单个相干缓存)。 它提供这种保证的原因是因为它在英特尔上这样做相当便宜–Windows只关心基于英特尔的架构,而Posix则关注更多异国情调的东西。

C ++ 11定义了一个用于线程的内存模型,它说这是一个数据竞争(即volatile 不能确保一个线程中的读取相对于另一个线程中的写入进行排序)。 两个访问可以按特定顺序排序,按照未指定的顺序排序(标准可能说“不确定顺序”,我记不起来),或根本没有排序。 根本没有排序是不好的 – 如果两个未经过排序的访问中的任何一个是写入,那么行为是未定义的。

这里的关键是隐含的“然后”在“我从一个线程修改一个元素,然后读取它的线程没有注意到这个改变”。 你假设操作是按顺序排序的,但它们不是。 就读取线程而言,除非您使用某种同步,否则其他线程中的写入尚未发生。 实际上它比那更糟糕 – 您可能会从我刚刚写的内容中想到,它只是未指定的操作顺序,但实际上具有数据竞争的程序的行为是未定义的。

C

什么挥发物做:

  • 如果变量是从外部源(硬件寄存器,中断,不同的线程,回调函数等)修改的,则保证变量中的最新值。
  • 阻止对变量进行读/写访问的所有优化。
  • 当编译器没有意识到程序调用线程/中断/回调时,防止在多个线程/中断/回调函数之间共享的变量可能发生的危险优化错误。 (这在各种有问题的嵌入式系统编译器中尤为常见,当你遇到这个bug时,很难找到它。)

挥发性不是什么:

  • 它不保证primefaces访问或任何forms的线程安全。
  • 它不能用于代替互斥锁/信号量/警卫/关键部分。 它不能用于线程同步。

什么挥发性可能会或可能不会:

  • 编译器可以或可以不实现提供存储器屏障,以防止多核环境中的指令高速缓存/指令管道/指令重新排序问题。 除非编译器文档明确指出它,否则你永远不应该假设volatile会为你做这件事。

对于volatile ,只能在每次使用其值时重新读取变量。 它不保证架构的不同级别上存在的不同值/表示是一致的。

要拥有这样的保证,你需要C11和C ++ 1中关于primefaces访问和内存障碍的新实用程序。 许多编译器已经在扩展方面实现了这些。 例如,gcc系列(clang,icc等)具有以前缀__sync开头的内置__sync来实现这些。

Volatile Keyword仅保证编译器不会对此变量使用寄存器。 因此,对此变量的每次访问都将读取内存位置。 现在,我假设您的架构中的多个处理器之间存在缓存一致性。 因此,如果一个处理器写入并且其他处理器读取它,那么它应该在正常条件下可见。 但是,您应该考虑角落案例。 假设变量位于一个处理器内核的管道中,而其他处理器正在尝试读取它,假设已经写入,则存在问题。 基本上,共享变量应该由锁保护,或者应该通过正确使用屏障机制来保护。

volatile的语义是实现定义的。 如果编译器知道在执行某段代码时将禁用中断,并且知道在目标平台上除了中断处理程序之外没有任何其他方法可以通过哪些操作来监视某些存储,它可以注册缓存volatile -这种存储中的限定变量与缓存普通变量的变量相同,前提是它记录了这种行为。

注意,行为的哪些方面被计为“可观察”可以通过实现在某种程度上定义。 如果实现文档表明它不打算在使用主RAM访问的硬件上使用以触发所需的外部可见操作,那么对该实现的访问将不会是“可观察的”。 如果没有人关心是否真正看到任何这样的访问,那么该实现将与能够物理地观察这种访问的硬件兼容。 但是,如果需要这样的访问,就像访问被视为“可观察”一样,编译器将不会声称兼容性,因此不会对任何事情做出任何承诺。

对于C ++:

据我所知,编译器从不优化声明为volatile的变量。

你的前提是错的。 volatile是编译器的提示,实际上并不保证任何东西。 编译器可以选择阻止对volatile变量进行一些优化,但就是这样。

volatile不是锁,不要试图这样使用它。

7.1.5.1

7)[注意:volatile是对实现的暗示,以避免涉及对象的激进优化,因为对象的值可能会被实现无法检测到的方式更改。 有关详细语义,请参见1.9。 一般来说,volatile的语义在C ++中与在C中的相同.-注意事项]

易失性仅影响它前面的变量。 在你的例子中,这是一个指针。 你的代码:volatile long array [8],指向数组第一个元素的指针是volatile,而不是它的内容。 (对于任何类型的对象都一样)

你可以调整它, 如何在c ++中声明使用malloc创建的数组是volatile的

volatile关键字与C ++ 中的并发volatile完全没有关系! 它用于防止编译器使用先前的值,即编译器将在代码中每次访问时生成访问volatile值的代码。 主要目的是内存映射I / O. 但是,使用volatile对读取普通内存时CPU的作用没有影响:如果CPU没有理由相信内存中的值发生了变化,例如因为没有同步指令,它只能使用其中的值缓存。 要在线程之间进行通信,您需要一些同步,例如std::atomic ,锁定std::mutex等。

易失性左值的C ++访问和易失性对象的C访问是“抽象地”“可观察的” – 尽管在实践中 C行为是按照C ++标准而不是C标准。 非正式地, volatile声明告诉每个线程,无论任何线程中的文本如何,值都可能以某种方式发生变化。 在带有线程的标准下,没有任何关于另一个线程写入导致对象发生变化的概念,无论是否是易失性,共享与否,除了通过同步函数调用时的同步函数调用的共享变量区域。 volatile 线程共享对象无关

如果您的代码没有正确地同步您正在讨论的线程,那么您的一个线程读取其他线程所写的内容具有未定义的行为。 所以编译器可以生成它想要的任何代码。 如果您的代码正确同步,则其他线程的写入仅发生在线程同步调用中; 你不需要volatile的。

PS

标准说“构成对具有volatile限定类型的对象的访问权限是实现定义的”。 因此,您不能只假设对于每个通过一个赋值的每个解除对易失性左值或写入访问的解除引用都存在读取访问权限。

此外,(“抽象的”)“可观察的” volatile访问如何“实际”表现出来是实现定义的。 因此,编译器可能不会为与定义的抽象访问相对应的硬件访问生成代码。 例如,可能只有具有静态存储持续时间的对象和用特定标志编译的用于链接到特殊硬件位置的外部链接可以从程序文本外部改变,以便忽略其他对象的volatile

但是,在某些情况下,我注意到即使我从一个线程修改一个元素,读取它的线程也不会注意到这个变化。 它继续读取相同的旧值,就好像编译器已将其缓存在某处。

这不是因为编译器在某处缓存了它,而是因为读取线程从其CPU核心的缓存中读取,这可能与写入线程的缓存不同。 为确保跨CPU核心的值更改传播,您需要使用适当的内存屏障,并且您既不需要也不需要在C ++中使用volatile。