我知道当从几个线程或进程写入的内存位置读取时, volatile 关键字应该用于该位置,如下面的一些情况,但我想知道更多关于什么限制它是否真的能够用于编译器,基本上在处理这种情况时编译器必须遵循哪些规则,并且有任何例外情况,尽管可以同时访问内存位置,但程序员可以忽略volatile关键字。
volatile SomeType * ptr = someAddress;
void someFunc(volatile const SomeType & input){
//function body
}
答案 0 :(得分:19)
你知道的是假的。易失性不用于同步线程之间的内存访问,应用任何类型的内存栅栏或任何类型的内存。 volatile
内存上的操作不是原子操作,并且不保证它们具有任何特定顺序。 volatile
是整个语言中最容易被误解的设施之一。 “Volatile is almost useless for multi-threadded programming.”
volatile
用于连接内存映射硬件,信号处理程序和setjmp
机器代码指令。
它也可以以类似于const
的方式使用,这就是Alexandrescu在this article中使用它的方式。但不要搞错。 volatile
不会使您的代码神奇地保持线程安全。以这种特定方式使用,它只是一个工具,可以帮助编译器告诉您可能搞砸了。您仍然可以自行修复错误,volatile
扮演没有角色来修复这些错误。
假设您的类具有指向无法更改的内容的指针。你可以自然地使指针const:
class MyGizmo
{
public:
const Foo* foo_;
};
const
在这里为你做了什么?它对记忆没有任何作用。它不像旧软盘上的写保护标签。记忆本身它仍然可写。你无法通过foo_
指针写入它。所以const
实际上只是一种给编译器提供另一种方式的方法,可以让你知道什么时候可能搞乱了。如果你要写这段代码:
gizmo.foo_->bar_ = 42;
...编译器不允许它,因为它标记为const
。显然你可以通过使用const_cast
抛弃const
来解决这个问题,但如果你需要确信这是一个坏主意,那么对你没有任何帮助。 :)
Alexandrescu对volatile
的使用完全相同。它没有做任何事情以任何方式使内存以某种方式“线程安全” 。它的作用是让编译器以另一种方式让你知道什么时候搞砸了。您将真正“线程安全”的东西(通过使用实际同步对象,如互斥锁或信号量)标记为volatile
。然后编译器不允许您在非volatile
上下文中使用它们。它抛出编译器错误,然后你必须考虑和修复。你可以通过使用volatile
抛弃const_cast
来解决这个问题,但这就像扔掉const
- 邪恶一样邪恶。
我的建议是完全放弃volatile
作为编写多线程应用程序(编辑:)的工具,直到真的知道你在做什么以及为什么这样做。它有一些好处,但不是大多数人认为的方式,如果你错误地使用它,你可能会编写危险的不安全的应用程序。
答案 1 :(得分:10)
答案 2 :(得分:7)
将变量声明为volatile
意味着编译器无法对其可能已经完成的值做出任何假设,从而阻止编译器应用各种优化。本质上,它强制编译器在每次访问时从内存中重新读取值,即使正常的代码流不会更改该值。例如:
int *i = ...;
cout << *i; // line A
// ... (some code that doesn't use i)
cout << *i; // line B
在这种情况下,编译器通常会假设由于i
之间的值没有被修改,因此可以保留A行的值(例如在寄存器中)并打印相同的值B.但是,如果将i
标记为volatile
,则告诉编译器某些外部源可能已修改了A行和B行之间i
处的值,因此编译器必须从内存中重新获取当前值。
答案 3 :(得分:4)
volatile
排除的一个特殊且非常常见的优化是将一个值从内存缓存到一个寄存器中,并使用该寄存器进行重复访问(因为这比每次返回内存要快得多) )。
相反,编译器必须每次从内存中获取值(从Zach中得到一个提示,我应该说“每次”都受到序列点的限制)。
写序列也不能使用寄存器,只能稍后再写入最终值:每个写必须被推出到内存中。
为什么这有用?在某些体系结构中,某些IO设备将其输入或输出映射到存储器位置(即写入该位置的字节实际上在串行线路上输出)。如果编译器将其中一些写入重定向到仅偶尔刷新的寄存器,则大多数字节将不会进入串行线路。不好。使用volatile
可以防止出现这种情况。
答案 4 :(得分:1)
不允许编译器优化循环中易失性对象的读取,否则通常会这样做(即strlen())。
在固定地址读取硬件注册表时,它通常用于嵌入式编程,并且该值可能会意外更改。 (与“普通”记忆相反,除非由程序本身写入,否则不会改变......)
这是它的主要目的。
它也可以用来确保一个线程看到另一个线程写入的值的变化,但它在读取/写入所述对象时绝不保证原子性。