【问题标题】:Is this rule about volatile usage strict?这条关于 volatile 使用的规则是否严格?
【发布时间】:2010-11-12 20:20:59
【问题描述】:

我看过这句话:

一般规则是,如果您有 必须的原始类型变量 在多个线程之间共享, 声明这些变量 易变

来自this article,还有这句话:

一般来说,任何可能的数据 不带日期的异步应该是 声明为 volatile。

来自this page,现在考虑到这个引入的规则,我想知道你能否举一个例子,尽管存在对数据的异步访问,但声明数据易失性在实践中没有用,或者没有这样的例外case 和规则是严格的。

【问题讨论】:

  • @sbi: 另请参阅我对 Alexandrescu 使用 volatile 的看法。总而言之:它与异步访问或内存栅栏无关,它与让编译器捕获您可能搞砸的地方有关一切。就像您尝试为 const 变量赋值一样。
  • 我还在stackoverflow.com/questions/4136900/…的回复中添加了详细说明
  • @Pooria:这是你的总结:Pooria:“volatile 是否必须在多线程中使用?”其他人:“volatile 与多线程无关”Pooria:“我问的不是多线程”
  • @John Dibling_我问的是多线程,但不是你想的同步对内存的访问。
  • 我的理解是,将某些东西声明为 volatile 只会告诉编译器不要缓存它的值,因为它可能会在其他地方更改。它并没有告诉编译器不要重新排序代码,这就是这里的根本问题。所以声明为 volatile 可能 是必要的,但仅靠它来实现线程安全是不够的。在实践中,由于编译器的实现方式,它可能会起作用——但这可能还不是一个足够好的保证!对吗?

标签: c++ multithreading rules volatile


【解决方案1】:

我记得那篇文章发表的时候,我记得随后在 comp.lang.c++.moderated 上进行了无休止的讨论。

IIRC,Andrei 劫持了 volatile 关键字,以使用它来区分不同的函数重载。 (请参阅 this article by Scott Meyers 了解另一个这样的想法。)他所做的非常出色,因为 它可以让编译器在你搞砸时捕捉到你up 对对象的受保护和不受保护的访问(非常类似于编译器在您尝试修改常量时捕获的)。但除了它可以帮助您之外,它与实际保护对对象的并发访问无关

问题只是 90% 的人只看了一眼文章,他们看到的只是volatile 和同一篇文章中的“线程”。根据他们的知识,他们要么得出volatile 对线程有好处的错误结论(你似乎已经这样做了),要么他们因为他领导他人而大喊大叫得出错误的结论。
似乎很少有人能够真正彻底地阅读这篇文章并理解他的真实所作所为。

【讨论】:

  • 这绝对是 100% 正确的。 Alexandrescu 在使用volatile 方面非常出色,自从我阅读这篇文章以来,我就一直以这种方式使用它。它对我帮助很大。但老实说,我确实认为这篇文章被如此误解,如此随意地阅读,它对最先进的技术造成的*损害*远大于帮助它。
  • @Pooria:如果你在这里问的不是 cmets 对你其他问题的回答,我仍然不知道你在这里问什么。然后你可能想澄清你的问题。
  • @Pooria:我们不是来和你打架的。我们在这里为您帮助。如果我和 sbi 正在回答您没有提出的问题,那么请重述问题
  • @Pooria:不会代表 sbi,但这是我的困惑。在这个线程中,您说“如果您有必须在多个线程之间共享的原始类型的变量,请将这些变量声明为 volatile”然后我们回答“volatile 与多线程无关”。然后你说“我不是在问多线程”。如果您不是在问多线程,那么您在问什么?
【解决方案2】:

我不能说实际异步访问场景,因为我不太擅长多线程,但是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 永远不会被修改,所以我什至不需要生成检查它的代码!”,而实际上我们只是在秘密修改它。

(实际上,这可能仍然适用于未优化的构建。并且,如果编译器很聪明并且注意到正在使用的指针,它也可能仍然有效。但是,原理是一样的)

【讨论】:

  • +1 @Mike - 基本上它正在做的事情(据我的理解)是强制编译器生成代码,以便每次访问时都会“查找”该值,因此没有循环不变提升或类似的东西。
  • 我添加了一个例子来证明这一点。
  • @Pooria:我不希望这样 :P 没有人应该写出像我刚才所做的那样的东西 :)
  • 现在等一下。如果不修改keepRunning就是死循环,那么volatile的意义何在?
  • @VJo keepRunning 在 while 循环中取消引用的 notAPointer 行中被修改。 volatile 的要点是编译器理论上可以查看提供的代码并确定 keepRunning 没有被修改,因此不会在每次循环迭代时检查它(或者在进入循环之前检查并且之后什么都不做),volatile 应该强制检查每次迭代的keepRunning。
【解决方案3】:

阅读this。 volatile 与多线程无关。

【讨论】:

  • 等等:链接文章的第 5 节基本上说“Volatile 与多线程编程无关”。这篇文章是由同一个人共同撰写的,他在 3.5 年前写道:“如果您有必须在多个线程之间共享的原始类型变量,请将这些变量声明为 volatile。”
  • 我只能推测亚历山德雷斯库明显改变了主意,但他似乎改变了主意。这也是一件好事:虽然我不会说它不好,但最初的建议肯定被误解了,并且在使用volatile时给了许多程序员一种错误的安全感。
  • @John:当第一篇文章出现时,Andrei 对 c.l.c++.m 产生了很大的影响,其中大部分是由于误解。我想这让他后来为了安全起见说“易失性与多线程编程无关”。他的原始文章仍然是正确的,但如果他劫持了volatile 以使编译器告诉他他什么时候搞砸了,比如系统编码和 UTF-8 字符串,那也同样正确。到目前为止,即使在第一篇文章中,他对volatile 的使用也与线程无关。
【解决方案4】:

要跟进 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 变量。

【讨论】:

  • @VJo:你什么意思?当然,它与多线程没有直接关系,但它确实与异步访问有关。
  • @VJo:你看报纸了吗?它讨论了volatile 在多线程环境中的不足。但是,异步 != 多线程。 volatile 与多线程无关,它与异步访问(包括内存映射 I/O 之类的东西)有关。
【解决方案5】:

我会提出一个更严格但非常有用的规则:如果您不完全了解volatile 的作用,请不要使用它。相反,请使用lock。如果您不完全了解 lock 的作用以及如何使用它,请不要使用多线程。

【讨论】:

  • 不幸的是,没有volatile 的锁定可能会导致编译器进行优化,从而导致您的程序错过对正确锁定的变量的更新。
【解决方案6】:

我想说,理论上这些规则是绝对正确的,每次当一个变量被 2 个线程访问时,都需要 volatile。 (即使使用互斥体,因为它们不会阻止编译器优化。)但实际上编译器足以识别变量可能在特定函数之外被修改的情况,因此它的值不应该被缓存在寄存器中。在大多数情况下,不需要 volatile。

我曾经在 MSVC 中通过检查不同情况下的汇编器输出进行了一些测试,而防止变量被缓存所需要的只是让另一个函数写入同一个变量或获取它的地址。除非打开“整个程序优化”,否则永远不会优化全局变量(因此编译器可以确定变量没有在其他翻译单元中修改)。

【讨论】:

  • @Timo_So 你告诉只声明任何原始类型的变量,甚至不声明 volatile 然后通过线程创建 API 将其地址传递给另一个线程,该变量不会被缓存?跨度>
【解决方案7】:

同样,C++ 标准没有指定 volatile 应该如何工作,您必须查看特定编译器对特定平台的作用。 volatile 也有点微妙,它的作用取决于编译器和目标平台的内存模型。锁使用起来更加直观。

【讨论】:

    【解决方案8】:

    在您过于认真地对待您首先链接到的文章之前,您可能需要阅读another。在后来的posting on Usenet 中,安德烈后来承认原始文章的某些部分完全是错误的,并指出这篇文章给出了更现实的看法(尽管请注意,他在那里给出的链接似乎已经过时了)。

    【讨论】:

      猜你喜欢
      • 2020-04-11
      • 1970-01-01
      • 2019-09-05
      • 1970-01-01
      • 2016-10-09
      • 1970-01-01
      • 2021-12-10
      相关资源
      最近更新 更多