为什么挥发性不够?

时间:2009-02-05 18:01:21

标签: c# synchronization

我很困惑。我的previous question的答案似乎证实了我的假设。但正如所述,here volatile不足以确保.Net中的原子性。 MSIL中的递增和赋值等操作不会直接转换为单个本机OPCODE,或者许多CPU可以同时读取和写入相同的RAM位置。

澄清:

  1. 我想知道多个CPU上的写入和读取是否是原子的?
  2. 我明白挥发性是什么。但这够了吗?如果我想获得其他CPU写入的最新值,是否需要使用互锁操作?

7 个答案:

答案 0 :(得分:10)

Herb Sutter最近写了一篇关于volatile的文章,以及它在本机C ++中的真正含义(它如何影响内存访问和原子性的排序)。 .NET和Java环境。这是一个很好的阅读:

答案 1 :(得分:6)

.NET中的volatile确实可以访问变量atomic。

问题是,这通常是不够的。如果你需要读取变量,如果它是0(表示资源是空闲的),你将它设置为1(表示它被锁定,其他线程应远离它)。

读0是原子的。写1是原子的。但在这两个行动之间,任何事情都可能发生。您可能读取0,然后在写入1之前,另一个线程跳入,读取0,并写入1。

但是,.NET 中的volatile确实保证了对变量的访问的原子性。它只是不保证依赖于多次访问的操作的线程安全性。 (免责声明:C / C ++中的volatile甚至不能保证这一点。只是你知道。它更弱,偶尔会出现bug,因为人们认为它保证了原子性:))

因此,您还需要使用锁定,将多个操作组合在一起作为一个线程安全的块。 (或者,对于简单的操作,.NET中的Interlocked操作可以做到这一点)

答案 2 :(得分:5)

我可能会在这里跳枪,但听起来好像你在这里混淆了两个问题。

一个是原子性,在我看来,这意味着单个操作(可能需要多个步骤)不应该与另一个这样的单个操作发生冲突。

另一个是波动性,这个值何时会发生变化,以及原因。

拿第一个。如果您的两步操作要求您读取当前值,修改它并将其写回,那么您肯定会想要一个锁,除非整个操作可以转换为可以在一个CPU上工作的单个CPU指令。单个缓存行数据。

然而,第二个问题是,即使你正在做锁定事情,其他线程会看到什么。

.NET中的volatile字段是编译器知道可以在任意时间更改的字段。在单线程世界中,变量的变化是在顺序指令流中的某个点发生的,因此编译器知道何时添加了更改它的代码,或者至少在它向外部世界呼叫时可能会也可能不会更改它,以便一旦代码返回,它可能与调用之前的值不同。

这一知识允许编译器在循环或类似的代码块之前将字段中的值提升到寄存器中,并且永远不会从该字段中重新读取该特定代码的值。

但是,使用多线程可能会给您带来一些问题。一个线程可能已经调整了值,而另一个线程由于优化而不会在一段时间内读取该值,因为它知道它没有改变。

因此,当您将字段标记为volatile时,您基本上告诉编译器它不应该假设它在任何时候都具有此值的当前值,除了每次需要值时抓取快照

锁定解决了多步操作,波动率处理编译器如何将字段值缓存到寄存器中,并且它们将共同解决更多问题。

另请注意,如果某个字段包含无法在单个cpu-instruction中读取的内容,那么您很可能也希望锁定对它的读取权限。

例如,如果你使用32位cpu并写入64位值,那么写操作将需要两个步骤来完成,如果另一个cpu上的另一个线程设法读取64位值在第2步完成之前,它将获得前一个值的一半和新的一半,很好地混合在一起,这可能比得到过时的更糟糕。


编辑:要回答评论,volatile保证了读/写操作的原子性,这在某种程度上是正确的,因为volatile关键字不能应用于大于32位的字段,实际上使字段single-cpu-instruction在32位和64位cpu上都可读/写。是的,它会阻止价值尽可能地保存在寄存器中。

因此部分评论错误,volatile无法应用于64位值。

另请注意,volatile有一些关于读/写重新排序的语义。

有关相关信息,请参阅MSDN documentationC# specification,找到here,第10.5.3节。

答案 3 :(得分:1)

在硬件级别上,多个CPU永远不能同时写入相同的原子RAM位置。原子读/写操作的大小取决于CPU架构,但在32位架构上通常为1,2或4个字节。但是,如果您尝试回读结果,则另一个CPU总是有可能写入中间的相同RAM位置。在较低级别,自旋锁通常用于同步对共享内存的访问。在高级语言中,可以称这种机制为例如关键区域。

volatile类型只是确保变量在更改时立即写入内存(即使该值在同一函数中使用)。如果稍后要在同一函数中重用该值,编译器通常会尽可能长时间地在内部寄存器中保留一个值,并在完成所有修改或函数返回时将其存储回RAM。在写入硬件寄存器时,或者当您想要确保将值存储回RAM中时,易失性类型最有用。多线程系统。

答案 4 :(得分:0)

你的问题并不完全有意义,因为volatile specifies the how the read happens,而不是多步骤过程的原子性。我的车也没有修剪我的草坪,但我尽量不坚持这个。 :)

答案 5 :(得分:0)

问题在于基于寄存器的变量值的兑现副本。

当读取一个值时,cpu会在检查主内存(较慢)之前首先查看它是否在寄存器中(快速)。

Volatile告诉编译器尽快将值推送到主内存,而不是信任缓存的寄存器值。它仅在某些情况下有用。

如果您正在寻找单个操作码写入,则需要使用Interlocked.Increment相关方法。但是它们在单个安全指令中的功能相当有限。

最安全,最可靠的赌注是锁定()(如果你不能做联锁。*)

编辑:如果它们处于锁定或互锁。*语句中,则写入和读取是原子的。根据你的问题条款,仅靠挥发性是不够的

答案 6 :(得分:-1)

Volatile是一个编译器关键字,告诉编译器要做什么。它不一定转化为(基本上)原子性所需的总线操作。这通常由操作系统决定。

编辑:澄清一下,如果你想保证原子性,那么volatile就永远不够了。或者更确切地说,由编译器决定是否足够。