在C#的多线程中,为什么要将bool变量声明为volatile?

7
我相当了解C++中的volatile关键字。但在C#中,它似乎有着不同的含义,更多地与多线程相关。我以为bool运算是原子性的,如果操作是原子性的,那么就不需要考虑线程问题了。我错了吗? https://msdn.microsoft.com/en-us/library/x13ttww7.aspx

1
volatile 防止变量访问周围的指令重排。这是相当复杂的。请参见 http://www.albahari.com/threading/part4.aspx#_Memory_Barriers_and_Volatility - xanatos
Eric Lippert在另一个问题的回答中实际上非常好地描述了C++和C#之间volatile语义的差异。 - Mike Zboray
2个回答

6

我曾认为bool操作是原子的

它们确实是原子的。

而且我认为如果操作是原子的,你就不必担心线程问题了。

这就是你对此的不完整认识。想象一下,你有两个线程在分别运行,每个线程都有自己的缓存层。线程#1在其缓存中有foo,而线程#2更新了foo的值。除非foo被标记为volatile,获得了lock,使用了Interlocked类或显式调用了Thread.MemoryBarrier(),否则线程#1将看不到更改,因为这将导致缓存中的值无效。从而保证你读取到最新的值

使用volatile修饰符确保一个线程检索另一个线程写入的最新值。Eric Lippert在他的优秀文章中解释了关于易变性的内容:
“易变读取和写入的真实语义比我在这里概述的要复杂得多;实际上,它们并没有真正保证每个处理器停止正在进行的操作并更新高速缓存/从主内存读取。相反,它们提供了有关如何观察到读取和写入之前和之后的内存访问如何相对有序的较弱保证。” 编辑: 根据@xanatos的评论,这并不意味着 volatile 保证立即读取,而是保证读取最新值。

部分错误。volatile写入并不能保证立即写入。 - xanatos
@xanatos 我不是指它保证立即写入,而是指它保证读取更新后的值。 - Yuval Itzchakov
你在写volatile时遇到了问题 :-) 写错了两次,错了两次 :-) - xanatos
@xanatos 一坨屎 :X。已修复。 - Yuval Itzchakov

4
在C#中,volatile关键字涉及到读/写重排,因此它是一些相当深奥的东西。来自这里(我认为这是有关线程的“圣经”之一)的内容如下:

volatile关键字指示编译器在每次从该字段读取时生成一个获取栅栏,并在每次写入该字段时生成一个释放栅栏。获取栅栏可以防止其他读/写在栅栏之前被移动;释放栅栏可以防止其他读/写在栅栏之后被移动。这些“半栅栏”比全栅栏更快,因为它们给运行时和硬件更多的优化空间。

这很难懂 :-)
现在... 它不意味着:
  • 值将立即被读取/写入
它只是意味着如果您从一个易失性变量中读取某些内容,则在此“特殊”读取之前已读/写入的所有其他内容都不会在此“特殊”读取之后被移动。所以它创建了一个屏障。因此,具有讽刺意味的是,通过从易失性变量中读取,您保证在读取时对任何其他变量(易失性或非易失性)所做的所有写操作都将完成。
易失性写入可能更重要,这在某种程度上是由英特尔CPU保证的,并且是Java的第一个版本没有保证的内容:无写入重排。问题是:
object myrefthatissharedwithotherthreads = new MyClass(5);

在哪里

class MyClass
{ 
    public int Value; 
    MyClass(int value) 
    { 
        Value = value; 
    } 
}

现在...那个表达式可以想象成:

var temp = new MyClass();
temp.Value = 5;
myrefthatissharedwithotherthreads = temp;

这里的temp是编译器生成的你看不见的内容。

如果写操作可以被重新排序,那么可能会出现以下情况:

var temp = new MyClass();
myrefthatissharedwithotherthreads = temp;
temp.Value = 5;

另一个线程可能会看到一个部分初始化的MyClass,因为在类MyClass完成初始化之前,myrefthatissharedwithotherthreads的值是可读的。


1
不仅仅是围栏,volatile 还可以防止像常量传播、死代码消除或寄存器提升这样的优化。它比简单的访问围栏更加严格。 - user703016
@Cicada,你有这方面的C#参考资料吗? - xanatos
一个获取-栅栏会阻止其他读取/写入在栅栏之前被移动,然后你说,之前已经读取/写入的所有其他内容在这个“特殊”读取之后不会被移动。我很难将这两者合并起来。它们似乎相互矛盾。 - pooya13

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