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

时间:2012-10-03 14:10:35

标签: c++ c multithreading x86 volatile

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

volatile long array[8];

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

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

10 个答案:

答案 0 :(得分:7)

  

但是in principal中的编译器不应该缓存volatile变量,对吗?

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

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

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

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

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

答案 1 :(得分:3)

<强> C

挥发性有什么作用:

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

什么挥发性不会:

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

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

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

答案 2 :(得分:2)

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

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

答案 3 :(得分:2)

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

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

对于C ++:

  

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

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

volatile不是锁定,请勿尝试使用它。

7.1.5.1

  

7)[注意:volatile是一个暗示要避免的实现   因为价值而涉及对象的积极优化   对象可能会被实现无法检测的方式更改。   有关详细语义,请参见1.9。一般来说,volatile的语义   C ++中的内容与C语言中的相同.-注释]

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

你可以适应它 How do I declare an array created using malloc to be volatile in c++

答案 8 :(得分:0)

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

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

PS

标准说“什么构成了对象的访问权限 具有volatile限定类型是实现定义的。“因此,您不能只假设每次解除引用易失性左值或每次赋值的写访问都有读访问权。

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

答案 9 :(得分:-1)

  

然而,在某些情况下,我注意到即使我修改了   来自一个线程的元素,读取它的线程没有注意到   更改。它继续读取相同的旧值,就像编译器一样   把它缓存到某处。

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