12

我知道当从多个线程或进程写入的内存位置读取时,应该将volatile关键字用于该位置,如下面的某些情况,但我想更多地了解它对编译器的真正限制以及基本上是什么规则编译器在处理这种情况时是否必须遵循,是否存在任何例外情况,尽管同时访问内存位置,但程序员可以忽略 volatile 关键字。

volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
 //function body
}
4

5 回答 5

19

你知道的都是假的。Volatile用于同步线程之间的内存访问、应用任何类型的内存栅栏或任何类似的东西。对volatile内存的操作不是原子的,也不保证它们是按任何特定的顺序进行的。 volatile是整个语言中最容易被误解的设施之一。“ Volatile 对于多线程编程几乎没有用处。

volatile用于连接内存映射硬件、信号处理程序和机器setjmp代码指令。

它也可以以类似的方式const使用,这就是 Alexandrescu 在本文中使用它的方式。但不要搞错。 volatile不会使您的代码神奇地线程安全。以这种特定方式使用,它只是一个工具,可以帮助编译器告诉你你可能在哪里搞砸了。纠正错误仍然取决于您,并且在纠正这些错误volatile没有任何作用。

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

假设你有一个类,它有一个指向不能改变的东西的指针。您可能会自然地将指针设为 const:

class MyGizmo
{ 
public:
  const Foo* foo_;
};

在这里真正为您做什么const?它对内存没有任何作用。它不像旧软盘上的写保护标签。内存本身仍然是可写的。你只是不能通过foo_指针写入它。所以const实际上只是给编译器另一种方式让你知道什么时候你可能会搞砸的一种方式。如果您要编写此代码:

gizmo.foo_->bar_ = 42;

...编译器不允许这样做,因为它被标记为const. 显然,您可以通过使用const_castto 摆脱const-ness 来解决此问题,但如果您需要确信这是一个坏主意,那么对您没有任何帮助。:)

Alexandrescu 的用法volatile完全一样。它不会以任何方式使内存以某种方式“线程安全” 。它的作用是它为编译器提供了另一种方式,让您知道您何时可能搞砸了。您将真正“线程安全”的东西(通过使用实际的同步对象,如互斥锁或信号量)标记为volatile. 然后编译器不会让你在非volatile上下文中使用它们。它会引发编译器错误,然后您必须考虑并修复。volatile您可以再次通过使用 丢弃-ness 来绕过它const_cast,但这与丢弃 -ness 一样邪恶const

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

于 2010-11-09T18:21:42.907 回答
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;
    

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

    a = 42; b = a;
    

    或者如果它可以像通常那样(在没有 的情况下volatile)生成

    a = 42; b = 42;
    

    (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();
    

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

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

于 2010-11-09T18:25:23.557 回答
7

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

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

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

于 2010-11-09T18:13:15.757 回答
7

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

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

一系列写入也不能使用寄存器,并且只能在稍后将最终值写回:每次写入都必须推送到内存中。

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

于 2010-11-09T18:31:21.210 回答
1

不允许编译器在循环中优化对 volatile 对象的读取,否则它通常会这样做(即 strlen())。

当在固定地址读取硬件注册表时,它通常用于嵌入式编程,并且该值可能会意外更改。(与“正常”内存相比,除非由程序本身写入,否则不会改变......)

这就是它的主要目的。

它也可以用来确保一个线程看到另一个线程写入的值的变化,但它绝不保证读取/写入所述对象时的原子性。

于 2010-11-09T18:13:56.570 回答