13

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

volatile long array[8];

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

注意:我不volatile用于线程同步,所以请不要再给我答案,例如使用锁或原子变量。我知道易失性、原子变量和互斥体之间的区别。另请注意,该架构是具有主动缓存一致性的 x86。此外,在它被另一个线程修改后,我读了足够长的时间。即使过了很长时间,读取线程也看不到修改后的值。

4

10 回答 10

7

但是编译器原则上不应该缓存 volatile 变量,对吗?

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

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

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

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

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

于 2012-10-03T14:20:52.637 回答
4

C

volatile 的作用:

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

什么 volatile 没有:

  • 它不保证原子访问或任何形式的线程安全。
  • 它不能用来代替互斥体/信号量/保护/临界区。它不能用于线程同步。

volatile 可能会或可能不会做的事情:

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

volatile你只能强加一个变量在你使用它的值时被重新读取。它不能保证在您的架构的不同级别上存在的不同值/表示是一致的。

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

于 2012-10-03T14:17:48.743 回答
2

Volatile关键字只保证编译器不会对这个变量使用寄存器。因此,对这个变量的每次访问都会去读取内存位置。现在,我假设您在架构中的多个处理器之间具有缓存一致性。因此,如果一个处理器写入而另一个处理器读取它,那么它在正常情况下应该是可见的。但是,您应该考虑极端情况。假设变量在一个处理器内核的管道中,而另一个处理器试图读取它并假设它已被写入,那么就会出现问题。所以本质上,共享变量应该被锁保护,或者应该通过正确使用屏障机制来保护。

于 2012-10-03T14:22:01.770 回答
2

的语义volatile是实现定义的。如果编译器知道在执行某些代码时中断将被禁用,并且知道在目标平台上除了中断处理程序之外没有其他方法可以观察到某些存储上的操作,它可以 register-cache volatile-qualified这种存储中的变量就像它可以缓存普通变量一样,只要它记录了这种行为。

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

于 2017-02-19T00:01:12.787 回答
1

对于 C++:

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

你的前提是错误的。volatile是对编译器的提示,实际上并不保证任何事情。编译器可以选择阻止对volatile变量进行一些优化,但仅此而已。

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

7.1.5.1

7) [注意: volatile 是对实现的提示,以避免涉及对象的激进优化,因为对象的值可能会通过实现无法检测到的方式进行更改。详细语义见 1.9。一般来说, volatile 的语义在 C++ 中与在 C 中的语义相同。 —尾注]

于 2012-10-03T14:12:25.833 回答
1

volatile关键字与 C++中的并发完全无关!它用于阻止编译器使用先前的值,即编译器将生成访问代码中每次访问值的代码。主要目的是诸如内存映射 I/O 之类的东西。但是,在读取普通内存时,使用不会影响CPU 的操作:如果 CPU 没有理由相信内存中的值发生了变化,例如,因为没有同步指令,它可以直接使用其缓存中的值. 要在线程之间进行通信,您需要一些同步,例如 an 、 lock a等。volatilevolatilestd::atomic<T>std::mutex

于 2012-10-03T14:25:52.367 回答
1

Volatile 只影响它前面的变量。在您的示例中,这里是一个指针。您的代码:volatile long array[8],指向数组第一个元素的指针是 volatile,而不是它的内容。(任何种类的物体都一样)

您可以像在 如何将使用 malloc 创建的数组声明为在 C++ 中易失一样进行调整

于 2014-03-06T10:02:00.857 回答
0

通过 volatile 左值的 C++ 访问和对 volatile 对象的 C 访问是“抽象的”“可观察的”——尽管实际上C 行为符合 C++ 标准而不是 C 标准。非正式地,volatile声明告诉每个线程该值可能会以某种方式改变,而不管任何线程中的文本如何。在线程标准下,除了在同步关键开始时同步函数调用的共享变量之外,没有任何另一个线程写入导致对象更改的概念,无论是否易失,是否共享地区。volatile与线程共享对象无关。

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

附言

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

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

于 2017-02-20T06:49:54.287 回答
-1

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

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

于 2012-10-03T14:23:46.643 回答