在处理易失性存储器位置时,编译器必须遵循什么规则?

Poo*_*ria 12 c++ memory compiler-construction rules volatile

我知道当从几个线程或进程写入的内存位置读取时,volatile属性应该用于该位置,就像下面的一些情况一样,但我想知道更多关于它对编译器的实际限制,基本上是什么规则编译器在处理这种情况时必须遵循,并且有任何例外情况,尽管可以同时访问存储器位置,但程序员可以忽略volatile关键字.

volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
 //function body
}
Run Code Online (Sandbox Code Playgroud)

Joh*_*ing 19

你知道的是假的.Volatile 用于同步线程之间的内存访问,应用任何类型的内存防护,或任何类型的内存.对volatile内存的操作不是原子操作,并且不保证它们具有任何特定顺序. volatile是整个语言中最容易被误解的设施之一." 易失性对于多线程编程几乎没用. "

什么volatile是用于与内存映射的硬件,信号处理程序和接口setjmp的机器代码指令.

它也可以以类似的方式const使用,这就是Alexandrescu在本文中使用它的方式.但不要搞错. volatile不会让您的代码神奇地保持线程安全.以这种特定方式使用,它只是一个工具,可以帮助编译器告诉您可能搞砸了.它仍然是你来修正自己的错误,并volatile起到任何作用在固定这些错误.

编辑:我会尝试详细说明我刚才所说的内容.

假设你有一个类,它有一个指向无法改变的东西的指针.你可能会自然地使指针const:

class MyGizmo
{ 
public:
  const Foo* foo_;
};
Run Code Online (Sandbox Code Playgroud)

const对你真的有什么用?它对记忆没有任何作用.它不像旧软盘上的写保护标签.记忆本身它仍然可写.你无法通过foo_指针写入它.因此const,这只是一种给编译器提供另一种方法的方法,可以让您知道何时可能会搞乱.如果你要写这段代码:

gizmo.foo_->bar_ = 42;
Run Code Online (Sandbox Code Playgroud)

...编译器不会允许它,因为它被标记了const.很显然,你可以通过使用const_cast去除const- 这来解决这个问题,但如果你需要确信这是一个坏主意,那么对你没有任何帮助.:)

Alexandrescu的用法volatile完全相同.它没有做任何事情以任何方式使内存以某种方式"线程安全" .它的作用是让编译器以另一种方式让你知道什么时候搞砸了.你标记你真正"线程安全"的东西(通过使用实际的同步对象,如互斥锁或信号量)volatile.然后编译器不允许您在非volatile上下文中使用它们.它抛出编译器错误,然后你必须考虑和修复.你可以通过抛弃volatile-ness来再次绕过它const_cast,但这就像邪恶一样抛弃了const- .

我的建议是完全放弃volatile编写多线程应用程序(编辑:)的工具,直到你真正知道自己在做什么以及为什么这样做.它有一些好处,但不是大多数人认为的方式,如果你错误地使用它,你可以编写危险的不安全的应用程序.

  • @Pooria:在你的第一句话中. (2认同)
  • @Pooria:这里:“当从多个线程或进程写入的内存位置读取时,应该在该位置使用 volatile 关键字”这个断言绝对是错误的。除了给你一种错误的安全感之外,“易失性”在这里对你没有任何作用。 (2认同)

zwo*_*wol 10

它的定义并不像你想要的那样好.C++ 98中的大多数相关标准都在1.9节"程序执行"中:

抽象机器的可观察行为是它对volatile数据的读写顺序以及对库I/O函数的调用.

访问由volatile左值(3.10)指定的对象,修改对象,调用库I/O函数或调用执行任何这些操作的函数都是副作用,这些都是执行环境状态的变化.表达的评估可能产生副作用.在称为序列点的执行序列中的某些特定点处,先前评估的所有副作用应该是完整的,并且不会发生后续评估的副作用.

一旦函数的执行开始,在完成被调用函数的执行之前,不会评估来自调用函数的表达式.

当通过接收信号中断抽象机器的处理时,volatile sig_atomic_t未指定类型以外的对象的值,并且volatile sig_atomic_t处理程序修改不属于该类型的任何对象的值变为未定义.

具有自动存储持续时间(3.7.2)的每个对象的实例与其块中的每个条目相关联.这样的对象存在并且在块的执行期间保持其最后存储的值并且当块被暂停时(通过调用函数或接收信号).

符合实施的最低要求是:

  • 在序列点处,volatile对象在先前评估完成且尚未发生后续评估的意义上是稳定的.

  • 在程序终止时,写入文件的所有数据应与根据抽象语义产生的程序执行的可能结果之一相同.

  • 交互设备的输入和输出动态应以这样一种方式进行,即在程序等待输入之前提示消息实际出现.构成交互设备的是实现定义的.

那么归结为:

  • 编译器无法优化对volatile对象的读取或写入.对于像提到的卡萨布兰卡这样的简单案例,它可以按照您的想法运作.但是,在像这样的情况下

    volatile int a;
    int b;
    b = a = 42;
    
    Run Code Online (Sandbox Code Playgroud)

    人们可以而且确实争论编译器是否必须像最后一行读取一样生成代码

    a = 42; b = a;
    
    Run Code Online (Sandbox Code Playgroud)

    或者如果它可以,通常会(在没有volatile)的情况下产生

    a = 42; b = 42;
    
    Run Code Online (Sandbox Code Playgroud)

    (C++ 0x可能已经解决了这一点,我还没有读完整篇文章.)

  • 编译器可能不会对volatile在单独的语句中出现的两个不同对象(每个分号是一个序列点)重新排序操作,但完全允许重新排列相对于易失性对象的非易失性对象的访问.这是你不应该尝试编写自己的自旋锁的众多原因之一,也是John Dibling警告你不要把它volatile当作多线程编程的灵丹妙药的主要原因.

  • 说到线程,您会注意到标准文本中完全没有提及线程.那是因为C++ 98没有线程概念.(C++ 0x确实如此,并且很可能指定它们的交互volatile,但如果我是你的话,我不会假设有人实现了这些规则.)因此,无法保证volatile从一个线程访问对象对另一个线程是可见的.线.这是另一个主要原因,volatile对于多线程编程并不是特别有用.

  • 无法保证volatile一次访问对象,或者对volatile对象的修改避免在内存中触摸它们旁边的其他内容.这在我所引用的内容中并不明确,但是暗示了这些内容volatile sig_atomic_t- sig_atomic_t否则该部分将是不必要的.这使得volatile访问I/O设备的效果远远低于预期,并且针对嵌入式编程进行销售的编译器通常提供更强的保证,但这不是您可以信赖的.

  • 很多人试图使对象的特定访问具有volatile语义,例如

    T x;
    *(volatile T *)&x = foo();
    
    Run Code Online (Sandbox Code Playgroud)

    这是合法的(因为它表示" 由易失性左值指定的对象"而不是" 具有易失性类型的对象")但必须非常小心地完成,因为记住我所说的编译器完全被允许重新排序非易失性相对于易变的访问?即使它是相同的对象(据我所知),即使这样也是如此.

  • 如果您担心重新排序访问多个易失性值,您需要了解序列点规则,这些规则冗长而复杂,我不打算在这里引用它们,因为这个答案已经太长了,但这里是一个很好的解释,只是一点点简化.如果您发现自己需要担心C和C++之间的序列点规则的差异,那么您已经搞砸了某些地方(例如,根据经验,从不重载&&).

  • 请注意,一些已知专家(特别是Herb Sutter)提到智能编译器甚至可以将`volatile`变量视为非易失性,如果它可以证明它不能从外部读取:考虑一个声明为volatile但未绑定到特定变量的变量地址,没有指针/引用传递给其他代码 - 示例:自动volatile变量,符合标准的编译器可以将其视为非易失性. (2认同)

cas*_*nca 7

声明变量volatile意味着编译器无法对其可能已经完成的值做出任何假设,从而阻止编译器应用各种优化.本质上,它强制编译器在每次访问时从内存中重新读取值,即使正常的代码流不会更改该值.例如:

int *i = ...;
cout << *i; // line A
// ... (some code that doesn't use i)
cout << *i; // line B
Run Code Online (Sandbox Code Playgroud)

在这种情况下,编译器通常会假设由于i未在两者之间修改值,可以保留A行的值(例如在寄存器中)并在B中打印相同的值.但是,如果您标记ivolatile,你告诉编译器一些外部源可能已经修改了iA行和B行之间的值,因此编译器必须从内存中重新获取当前值.


dmc*_*kee 5

排除在外的一种特殊且非常常见的优化volatile是将一个值从内存中缓存到一个寄存器中,并使用该寄存器进行重复访问(因为这比每次都返回到内存要快得多)。

相反,编译器每次都必须从内存中获取值(从Zach得到提示,我应该说“每次”都受序列点限制)。

一系列写操作也不能利用寄存器,只能在以后写最终值:每次写操作都必须推送到内存中。

为什么这有用?在某些体系结构上,某些IO设备会将其输入或输出映射到存储位置(即,写入该位置的字节实际上在串行线上消失)。如果编译器将其中一些写入重定向到仅偶尔刷新的寄存器,则大多数字节将不会进入串行行。不好。使用volatile可以防止这种情况。