我看过这句话:
一般规则是,如果您有必须在多个线程之间共享的原始类型变量,请将这些变量声明为 volatile
从这篇文章和这句话:
一般来说,任何可能被异步取消日期的数据都应该被声明为易失的。
从这个页面,现在考虑这个引入的规则,我想知道你能否举一个例子,尽管存在对数据的异步访问,但声明数据易失性在实践中没有用,或者没有这种例外情况和规则是严格的。
我记得那篇文章发表的时候,我记得随后在 comp.lang.c++.moderated 上进行了无休止的讨论。
IIRC,Andrei 劫持了volatile
关键字以使用它来区分不同的函数重载。(有关另一个这样的想法,请参阅Scott Meyers 的这篇文章。)他所做的非常出色,因为它允许编译器在您弄乱对对象的受保护和不受保护的访问时捕获您(非常类似于编译器捕获您应该尝试修改常数)。但除了它对您有帮助之外,它与实际保护对对象的并发访问无关。
问题只是 90% 的人只看一篇文章,他们看到的只是volatile
同一篇文章中的“线程”。然后,根据他们的知识,他们要么得出volatile
对线程有益的错误结论(您似乎已经这样做了),要么他们对他大喊大叫,因为他导致其他人得出错误的结论。
似乎很少有人能够真正彻底地阅读这篇文章并理解他的真实所作所为。
我不能说实际的异步访问场景,因为我不太擅长多线程,但是volatile
修饰符的作用是告诉编译器:
“听着,这可能随时改变,所以不要缓存它或将它放入寄存器或做任何疯狂的事情,好吗?”
它不能防止异步写入,如果变量可以被外力改变,它只会禁用无效的优化。
编辑:作为一个潜在的例子,一个不涉及多线程的例子(但是,确实涉及异常复杂的代码;),这是一个 volatile 很重要的情况:
volatile bool keepRunning = true;
void Stuff() {
int notAPointer = 0;
notAPointer = (int)(&keepRunning); //Don't do this! Especially on 64-bit processors!
while(keepRunning) {
*(bool*)(notAPointer) = false;
}
printf("The loop terminated!");
}
如果没有那个 volatile 修饰符,编译器可能会“嘿,keepRunning 永远不会被修改,所以我什至不需要生成检查它的代码!”,而实际上我们只是在秘密修改它。
(实际上,这可能仍然适用于未优化的构建。并且,如果编译器很聪明并且注意到正在使用的指针,它也可能仍然有效。但是,原理是相同的)
读这个。volatile 与多线程无关。
我想说,理论上这些规则是绝对正确的,每次当一个变量被 2 个线程访问时,都需要 volatile。(即使使用互斥锁,因为它们不会阻止编译器优化。)但实际上编译器足以识别变量可能在特定函数之外被修改的情况,因此它的值不应该被缓存在寄存器中。在大多数情况下,volatile 是不必要的。
我曾经在 MSVC 中通过检查不同情况下的汇编程序输出进行了一些测试,而防止变量被缓存所需要的只是让另一个函数写入同一个变量或获取它的地址。除非打开“整个程序优化”,否则永远不会优化全局变量(因此编译器可以确保变量没有在其他翻译单元中修改)。
为了跟进 Mike 的回答,它在这种情况下很有用(用于避免此示例复杂性的全局变量):
static volatile bool thread_running = true;
static void thread_body() {
while (thread_running) {
// ...
}
}
static void abort_thread() {
thread_running = false;
}
根据复杂thread_body
程度,编译器可能会选择thread_running
在线程开始运行时将 的值缓存在寄存器中,这意味着它永远不会注意到该值是否更改为 false。 volatile
强制编译器发出将检查thread_running
每个循环上的实际变量的代码。
我会提出一个更严格但非常有用的规则:如果你不明白到底是什么volatile
,不要使用它。相反,使用lock
. 如果您不完全了解lock
它的作用以及如何使用它,请不要使用多线程。
在您过于认真地对待您首先链接到的文章之前,您可能想阅读另一篇. 在后来在 Usenet 上的一篇帖子中,安德烈后来承认原始文章的某些部分完全是错误的,并指出这篇文章给出了一个更现实的看法(尽管请注意,他在那里给出的链接似乎已经过时了)。
同样,C++ 标准没有指定 volatile 应该如何工作,您必须查看特定编译器对特定平台的作用。volatile 也有点微妙,它的作用取决于编译器和目标平台的内存模型。锁使用起来更加直观。