【问题标题】:Usage of volatile specifier in C/C++/Java在 C/C++/Java 中使用 volatile 说明符
【发布时间】:2009-06-30 11:18:03
【问题描述】:

在浏览多线程编程的许多资源时,通常会出现对 volatile 说明符的引用。 很明显,至少在 C/C++ 和 Java(1.4 及更早版本)中,使用此关键字不是实现多线程之间同步的可靠方法。 以下是维基百科列出的(没有解释如何)作为这个说明符的典型用法:-

  1. 允许访问内存映射设备
  2. 允许在 setjmp 和 longjmp 之间使用变量
  3. 允许在信号处理程序中使用变量
  4. 忙着等待

我可以开始看到这个说明符在上面列出的用法中的作用,但是由于我还没有完全了解这些领域中的每一个,所以我无法弄清楚这个说明符在每个领域中的确切行为用法。

谁能解释一下?

【问题讨论】:

  • 感谢您的回答。关于 volatile 在上面列出的用法中究竟是如何工作的任何输入?

标签: java c++ volatile


【解决方案1】:

您的问题在技术上被称为“一罐蠕虫”! 对于 c/c++(我无法评论 java)
您可以非常粗略地将 volatile 概括为对编译器说“请不要优化它”的指令,但专业人士之间有很多争论,关于它是否是
a) At all useful for kernel level code b) Even implemented correctly by most compilers.

另外,永远不要将它用于多线程编程和here's a very good explanation as to why

=编辑= 有趣的是,它的价值。 Dennis Ritchie 反对包含(以及 const )详细信息here

【讨论】:

  • 需要注意的是,“At all有用”文档链接到linux内核论文。该文档讨论了在 linux 内核中使用的 volatile。在底部,它们仍然很好地使用了 volatile,这与多线程无关。 Volatile 是有意义的并且在那里很好(如果编译器正确处理 volatile),就像在一个繁忙的循环中,其中变量在同一个 cpu 的中断处理程序中更新。编译器只是不知道它是否不应该优化读取。您的观点听起来像是“易失性”完全是废话(尽管我知道您可能不打算这样做):)
  • @litb 是的,公平点。我会更清楚地说明这一点。我不会说这完全是废话,只是想我会指出这并不是那么明确
  • 正是我要说的。一个澄清是,优化通常是将值存储在寄存器中,然后从那里重用它。 Volatile 强制代码在每次读取时从内存中重新读取值,而不依赖于寄存器中的缓存值。现在编译器做了很多优化,包括重写语句和它们的顺序,所以如果你依赖其他线程中更新的顺序,仅仅从内存中重新读取一个值是不够的,要处理这个需要内存屏障。
【解决方案2】:

由于您对这些用例感兴趣,我将解释第一个。请注意,这适用于 c/c++ 的角度,不确定它如何在 java 中发挥作用,尽管我怀疑 c/c++ 和 java 中的 volatile 通常用于完全不同的情况。

内存映射设备是处理器以与内存相同的方式而不是通过特殊总线与之通信的外围设备。

假设您有一个带有内存映射计时器的小灯。您通过将 1 写入其内存地址来打开灯,其内部计时器倒计时 5 秒,然后关闭灯并将内存位置重置为 0。现在您正在开发一个需要在某些事件后打开灯的 c 程序,有时在计数器到期之前将其关闭。如果您使用常规变量(往往是此类应用程序的指针或引用)写入其内存位置,则由于编译器优化,可能会出现许多问题。

如果您没有使用那么多变量,并且您正在打开灯并在关闭灯后不久打开灯,而没有使用该值的任何其他变量 - 有时编译器会完全摆脱第一个赋值,或者在其他在这种情况下,它只会保持处理器寄存器中的值并且永远不会写入内存。在这两种情况下,灯都不会亮,因为它的内存从未改变。

现在想想另一种情况,您检查灯的状态并打开。在这里,该值是从设备的内存中提取的并保存在处理器寄存器中。现在,几秒钟后,灯会自行熄灭。此后不久,您尝试再次打开灯,但是由于您读取了该内存地址并且此后没有更改它,编译器假定该值仍然是 1,因此永远不会更改它,尽管它现在实际上是 0。

通过使用 volatile 关键字,您可以防止编译器在将您的代码转换为机器代码时做出任何这些假设,并确保所有这些特定操作都严格按照程序员编写的方式执行。这对于内存映射设备至关重要,主要是因为内存位置不会被处理器严格更改。出于同样的原因,具有共享内存的多处理器系统在公共内存空间上运行时通常需要类似的做法。

【讨论】:

    【解决方案3】:

    我发现 Herb Sutter 的这篇 DDJ 文章非常有趣,尤其是在 C++、Java 和 C# .NET 中如何处理 volatile。

    Dr.Dobbs volatile vs. volatile

    【讨论】:

      【解决方案4】:

      这里有一个很好的解释:http://en.wikipedia.org/wiki/Volatile_variable,但稍微简化了一点,它告诉编译器它不应该假设该变量未被其他人访问,并且将其优化为注册器并仅更新注册而不是实际的存储。

      【讨论】:

      • 它在 C 和 C++ 中与在 Java 中不同,这并不奇怪。例如在 Java 中,如果编译器可以确定 volatile 只能由单个线程访问,则可以将其视为非 volatile。
      • 正确,我认为他们在维基百科中也提到了这一点。然而,说明符的目的还是大致相同的。
      【解决方案5】:

      Volatile 变量在 Java 中很有用(至少从 Java 5.0 开始,它们的 behaviour changed),正如 Brian Goetz 在他的“Java 并发实践”(JCIP)一书中所说的那样 - 关于该主题的基本书籍(第 37 页):

      确保对变量的更新 可预测地传播到其他 线程

      显式同步也可以实现这一点,但通常我们并不总是想要锁定一个值。双重检查锁定是这方面的经典例子(复制自Wikipedia):

      // Works with acquire/release semantics for volatile
      // Broken under Java 1.4 and earlier semantics for volatile
      class Foo {
          private volatile Helper helper = null;
          public Helper getHelper() {
              if (helper == null) {
                  synchronized(this) {
                      if (null == helper)
                          helper = new Helper();
                  }
              }
              return helper;
          }
      
          // other functions and members...
      }
      

      如果助手不是 volatile,这将不起作用。

      Volatile 变量也可用于实现非锁定并发数据结构,例如 java.util.concurrent.ConcurrentHashMap(支持并发更新和无锁定访问 - 请参阅 JDK 源代码了解其对 volatile 的使用)。

      JCIP 对双重检查锁定、可变变量和 Java 并发进行了很好的讨论。 Joshua Bloch 的《Effective Java》第 2 版也值得一读。

      另请注意,java.util.concurrent.atomic 包中的 Java 支持原子变量。这些允许以与 volatile 变量类似的方式跨线程/处理器使值的更改可见,但也允许执行“比较和设置”操作,这意味着可以安全地执行某些其他类型的并发操作而无需锁定。

      【讨论】:

        【解决方案6】:

        volatile 关键字很久以前就出现在 C 语言中,它的作用基本上是“关闭”一些编译器优化,这些优化假设变量没有显式更改,它根本没有更改。当时它的主要用途是声明将由中断处理程序更改的变量。例如,我曾经(80 年代后期)将它用于包含鼠标光标位置的全局变量。位置被中断改变了,如果没有 volatile,主程序有时不会检测到它的变化,因为编译器优化了变量访问,认为没有必要。

        如今,这些用途通常已过时(除非您编写低级操作系统代码),但仍有一些罕见的情况下 volatile 有用(确实非常罕见 - 例如,我可能没有使用它过去 7 年)。

        但对于多线程编程,它是完全不推荐的。问题是它不会保护线程之间的并发访问,它只会删除会阻止其在同一线程中“刷新”的优化。它不适用于多线程环境。如果您使用的是 Java,请使用同步。如果您使用 C++,请使用一些同步库,例如 pthreads 或 Boost.Threads(或者,如果可以,最好使用新的 C++ 0X 线程库)。

        【讨论】:

          【解决方案7】:

          自从我完成 C++ 以来已经有一段时间了,我真的不记得该语言中 volatine 的定义。但是 Java 语言规范特别指出 volatile 的目的是为了方便多线程访问一个变量。引用:“一个字段可能被声明为 volatile,在这种情况下,Java 内存模型(第 17 节)确保所有线程都能看到变量的一致值。”他们继续说,保证对 volatile 值的引用按照它们在代码中指定的顺序得到满足,即如果你声明 i 和 j volatile 然后写“++i; ++j”,那么 i实际上,将始终在 j 之前递增。

          我记得唯一一次在 Java 中使用 volatile 是当我有一个线程可能设置一个取消标志而另一个线程循环执行一些大操作并且每次通过循环检查取消标志时。这确实如我所料。

          我同意“易失性”的用处非常有限。大多数多线程在某些时候需要“同步”。但“有限”和“无”不是一回事。余弦函数在大多数业务应用程序中的用处也非常有限。但是当你需要它的时候,哇,这样就省去了很多麻烦。

          【讨论】:

            【解决方案8】:

            当该变量可以被许多线程访问并且您希望在每条指令中您的代码都应该获取该变量的更新值时,应该使用易失性变量。

            编译器通常会优化代码并将变量存储在寄存器中,而不是在发现没有人更新时每次都从内存中获取。

            但是通过使用 volatile,您可以强制编译器每次都获取更新的值。

            【讨论】:

            • 这是不正确的。 'volatile' 几乎只是告诉编译器“你控制之外的东西可能会修改这个变量,所以不要优化它”。对于会发生什么,你真的没有其他保证。其他答案中的链接解释了为什么按照您的描述使用 'volatile' 是错误的。
            • 考虑 CPU 缓存。 C 抽象机不知道 cpu 缓存。例如,一个值可能在存储中,如果它只是在 one cpu 的 cpu 缓存中。对于多线程应用程序,使用这样的缓存意味着“存储”是徒劳的。
            • @Richard:同意。 volatile 说明符永远不应该用作实现多个线程之间同步的手段。
            • ...除了在单个 cpu 机器中,如果您仔细了解内存访问是原子的还是非原子的,它可以用于此目的。此类系统的数量大大超过多核桌面,但大多数情况下是不可见的,并且不会被那些不为它们开发的人轻易识别为计算机。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-07-13
            • 1970-01-01
            • 2012-09-17
            • 1970-01-01
            • 1970-01-01
            • 2020-12-14
            相关资源
            最近更新 更多