为什么挥发性不够?

Jac*_*icz 9 c# synchronization

我糊涂了.我之前的问题的答案似乎证实了我的假设.但正如此处所述, volatile不足以确保.Net中的原子性.MSIL中的增量和赋值等操作不会直接转换为单个本机OPCODE,或者许多CPU可以同时读取和写入相同的RAM位置.

澄清:

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

Mic*_*urr 10

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

  • 它*是一个非常好的阅读(绝对值得+1),但通常首选SO答案有点自足.如果您的答案包含该文章重要部分的摘要,那将是很好的,只有这样(1)浏览页面的人会看到它,以及(2)因此,如果文章被移动或删除,答案将保持相关性. (3认同)

jal*_*alf 6

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

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

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

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

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


ang*_*son 5

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

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

另一个是波动性,这个值何时会发生变化,为什么会发生变化.

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

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

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

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

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

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

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

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

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


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

因此部分注释是错误的,volatile不能应用于64位值.

还要注意,它volatile有一些关于读/写重新排序的语义.

对于相关的信息,请参阅MSDN文档C#规范,发现在这里,第10.5.3节.