使用volatile关键字相对于使用Interlocked类有什么优势?

8
换句话说,使用易失性变量能解决的问题,普通变量和Interlocked类也能解决吗?

我知道 volatile 的作用。问题是是否存在无法用 interlocked 方法替换 volatile 的情况? - codymanix
5个回答

17

编辑:问题已经被大量重写

为了回答这个问题,我深入研究了一下volatileInterlocked的一些事情,发现了一些我之前不知道的内容。让我们澄清一下这个问题,不仅是为了我自己,也是为了这个讨论和其他正在学习这方面知识的人们:

  • volatile读/写应该不受重新排序的影响。这只是指读和写操作,而不是任何其他操作;
  • 易失性没有被强制施加到CPU(即硬件级别),例如x86在任何读/写上使用获取和释放屏障。它确实防止编译器或CLR优化;
  • Interlocked使用原子汇编指令进行CompareExchange (cmpxchg)、Increment (inc)等操作;
  • Interlocked有时会使用锁:在多处理器系统上使用硬件锁; 在单处理器系统中,没有硬件锁;
  • Interlockedvolatile的区别在于它使用了一个全屏障,而volatile使用了半屏障。
  • 当您使用volatile时,写后读操作可能会被重新排序。这在使用Interlocked时不可能发生。`VolatileRead和VolatileWrite与volatile有相同的重新排序问题(感谢Brian Gideon提供的链接)。

现在我们有了规则,我们可以对你的问题给出一个答案:

  • 从技术上讲:是的,有一些你可以用volatile做而不能用Interlocked做的事情:
    1. 语法:你不能写a = b,其中ab是volatile,但这很明显;
    2. 由于重新排序,你可以在将值写入易失性变量后读取不同的值。这在Interlocked中是不可能的。换句话说,你可以使用volatile比使用Interlocked不安全
    3. 性能:volatileInterlocked更快。
  • 从语义上讲:不,因为Interlocked仅提供了一组操作的超集,并且更安全,因为它应用了全屏障。你不能使用volatile做任何Interlocked不能做的事情,而你可以使用Interlocked做许多volatile不能做的事情:

static volatile int x = 0;
x++;                        // non-atomic
static int y = 0;
Interlocked.Increment(y);   // atomic
  • 作用域: 是的,将变量声明为volatile会使得每次访问都是volatile的。除此之外,无法以其他方式强制这种行为,因此volatile不能被Interlocked替换。在其他库、接口或硬件可以随时访问并更新您的变量,或者需要最新版本的情况下,这是必需的。

  • 如果你问我,这个最后一部分才是volatile的实际需求,并且当两个进程共享内存并需要读取或写入而不加锁时,它可能是理想的选择。在这种情况下,将变量声明为volatile比强制所有程序员使用Interlocked更安全(编译器无法强制执行)。


    编辑:以下引用是我的原始回答的一部分,我将其保留;-)

    来自C# 程序设计语言标准的引用:

    对于非 volatile 字段,考虑重新排序指令的优化技术可能会导致在没有同步的情况下访问字段的多线程程序中出现意外和不可预测的结果,例如由lock语句 提供的同步。这些优化可以由编译器、运行时系统或硬件执行。对于 volatile 字段,这种重新排序优化是受限制的:

    • 读取 volatile 字段称为volatile读。volatile读具有“获取语义”,即在指令序列中它之后发生的任何内存引用都保证在其之前。

    • 写入 volatile 字段称为volatile写。volatile写具有“释放语义”,即在指令序列中在写入指令之前的任何内存引用都保证已发生。

    更新:问题大部分重写,纠正了我的原始回答并添加了一个“真实”的答案


    你说得对,感谢指出。查看CompareExchange(认为它不是原子操作),SSCLI(共享源代码CLI)显示它最终转换为x86处理器上的cmpxchg汇编指令,而该指令就像Interlocked中的其他任何内容一样,确实是原子操作。 - Abel
    那些更正,以及重写现在已经在答案中了 :) - Abel
    你说过,如果没有使用volatile关键字,你无法强制对变量进行线程安全访问。但是你可以定义一个属性,在其getter/setter中使用Interlocked.Read()和Interlocked.Exchange()方法。 - codymanix
    那是伪装变量。当然你可以这样做。但这并不改变你不能以这种方式拥有变量字段的事实。通过属性隐藏使字段不太容易出错,但在类内部和外部(通过反射打破OO),它仍然是可访问的,具有线程不安全性等问题。另一方面,无法将“易失性”属性的地址提供给某些外部函数(API、硬件或其他),您需要提交字段的地址,而Interlocked无法帮助您。 - Abel
    传递一个易失变量的引用会丢失其易失性,如果尝试这样做,编译器会发出警告。 不过,你的答案非常好而详尽 :) - codymanix
    显示剩余2条评论

    1

    这是一个相当复杂的话题。我发现Joseph Albahari的写作是.NET Framework中多线程概念方面更为明确和准确的来源之一,可能有助于回答您的问题。

    但是,简单地总结一下,volatile关键字和Interlocked类在它们的使用方式上有很多重叠。当然,它们都远远超出了普通变量所能做到的。


    非常棒的文章!阅读它需要一些时间,完全理解它则需要更长的时间。 - codymanix

    0

    可以直接查看这个值。

    只要您仅使用Interlocked类来访问变量,那么就没有区别。 volatile 的作用是告诉编译器该变量是特殊的,并且在优化时不应假定该值没有更改。

    看看这个循环:

    bool done = false;
    
    ...
    
    while(!done)
    {
    ... do stuff which the compiler can prove doesn't touch done...
    }
    

    如果你在另一个线程中将done设置为true,你会期望循环退出。然而,如果没有将done标记为volatile,那么编译器有可能意识到循环代码永远不会改变done,并且可以优化掉用于退出的比较。

    这是多线程编程中的难点之一 - 许多问题只会在特定情况下出现。


    您可以使用Interlocked.Read()来实现相同的功能。 - codymanix
    1
    你对volatile的描述有些错误。你描述的是C++中的volatile(不适用于多线程)。而在C#(和Java)中,volatile会在访问变量时插入同步操作。由于存在同步操作,它被强制每次重新读取变量,这具有类似于C++ volatile的效果。 - deft_code

    0

    我不会试图成为这个主题的权威,但我强烈建议您查看备受推崇的Jon Skeet所写的this article

    还要看一下this answer的最后一部分,其中详细说明了应该使用volatile的情况。


    Skeet的文章很好,但只涵盖了基础知识。我知道volatile有什么用,但问题是是否存在任何情况,你不能用Interlocked方法替换volatile? - codymanix

    -1

    是的,您可以使用volatile变量而不是锁来提高一些性能。

    锁是一个完整的内存屏障,它可以给您与volatile变量相同的特性以及许多其他特性。正如已经说过的,volatile只是确保在多线程场景中,如果CPU更改其缓存行中的值,其他CPU会立即看到该值,但并不保证任何锁定语义。

    问题是锁比volatile强大得多,您应该在可能的情况下使用volatile来避免不必要的锁定。


    我不是在谈论锁语句,而是Interlocked类,但是你说的是正确的。 - codymanix

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接