易变与联锁与locking

假设一个类有一个public int counter字段,可以被多个线程访问。 这个int只是递增或递减。

为了增加这个领域,应该使用哪种方法,为什么?

  • lock(this.locker) this.counter++;
  • Interlocked.Increment(ref this.counter);
  • counter的访问修饰符更改为public volatile

现在我发现了volatile ,我一直在删除许多lock语句和Interlocked的使用。 但有没有这样做的理由?

最差(实际上不会工作)

counter的访问修饰符更改为public volatile

正如其他人所说,这本身并不安全。 volatile点在于多个CPU上运行的多个线程可以caching数据并重新sorting指令。

如果它不是 volatile ,并且CPU A增加一个值,那么CPU B可能实际上看不到增加的值,直到一段时间后,这可能会导致问题。

如果是volatile ,这只能确保两个CPU同时看到相同的数据。 它不会阻止它们交错读取和写入操作,这是您试图避免的问题。

次好的:

lock(this.locker) this.counter++ ;

这是安全的(只要你记得lock其他地方,你访问this.counter )。 它阻止任何其他线程执行任何其他由locker守卫的代码。 使用锁也可以防止多CPU重新sorting的问题,这很好。

问题是,locking速度很慢,如果你在其他地方重新使用locker ,那么你可能会无缘无故地阻塞其他线程。

最好

Interlocked.Increment(ref this.counter);

这是安全的,因为它有效地进行了读取,增加和写入“一击”,不能被打断。 正因为如此,它不会影响任何其他代码,也不需要记住在别处locking。 这也是非常快(正如MSDN所说,在现代CPU上,这通常是一个单一的CPU指令)。

我不完全确定,但如果它绕过其他CPU重新sorting的东西,或者如果你还需要结合挥发性增量。

InterlockedNotes:

  1. 互锁方法在任何数量的核心或CPU上都是安全的。
  2. 联锁方法围绕他们执行的指令应用完整的围栏,所以重新sorting不会发生。
  3. 联锁方法不需要甚至不支持对易失性字段的访问 ,因为易失性字段在给定字段的操作周围放置了一半的栅栏,并且互锁使用了完整的栅栏。

脚注:什么波动是真正的好处。

由于volatile不能防止这种multithreading问题,这是什么? 一个很好的例子就是说你有两个线程,一个总是写入一个variables(比如queueLength ),一个总是读取同一个variables。

如果queueLength不是易失性的,则线程A可能会写入五次,但是线程B可能会将这些写入视为延迟(甚至可能是错误的顺序)。

解决办法是locking,但是你也可以在这种情况下使用volatile。 这将确保线程B将始终看到线程A写入的最新内容。 但是请注意,这个逻辑适用于没有阅读过的作家和从不写作的阅读者,如果你写的东西是一个primefaces值。 只要您执行一次读取 – 修改 – 写入,您需要进入联锁操作或使用locking。

编辑:正如在评论中指出的,这些天,我很高兴使用Interlocked单个variables的情况下,它显然是好的。 当它变得更复杂,我仍然会恢复locking…

当您需要增加时,使用volatile将无济于事,因为读取和写入是单独的指令。 读完之后,又写回来之前,另一个线程可能会改变这个值。

就个人而言,我几乎总是locking – 以一种明显正确的方式比波动性或互锁增长更容易。 就我而言,无锁multithreading是针对真正的线程专家,我不是其中之一。 如果Joe Duffy和他的团队build立了很好的库来平行进行事务,而没有像构build的东西那样locking,那就太棒了,我会用心跳 – 但是当我自己进行线程的时候,我试着去把事情简单化。

volatile ”不能代替Interlocked.Increment 。增加! 它只是确保variables不被caching,但直接使用。

增加一个variables实际上需要三个操作:

  1. 增量

Interlocked.Increment执行所有三个部分作为一个单一的primefaces操作。

locking或联锁增量是您正在寻找的。

Volatile绝对不是你所追求的 – 它只是告诉编译器把variables视为一直改变,即使当前代码path允许编译器优化从内存中读取的内容。

例如

 while (m_Var) { } 

如果m_Var在另一个线程中被设置为false,但是它没有被声明为volatile,编译器可以自由地使它成为一个无限循环(但并不意味着总会这样),通过检查一个CPU寄存器(例如EAX,因为那是什么m_Var从一开始被拿进来),而不是发出另一个读到m_Var的内存位置(这可能是caching – 我们不知道也不在乎,这是x86 / x64的caching一致性点)。 所有提到的指令重新sorting的人早些时候的post只是显示他们不了解x86 / x64架构。 挥发性不会产生读/写障碍,正如之前的post所说的那样,“防止重新sorting”。 事实上,再次感谢MESI协议,我们保证我们读取的结果总是在CPU上相同,无论实际结果是否已经退到物理内存或只是驻留在本地CPU的caching中。 我不会过分详细了解这一点,但请放心,如果出现这种情况,Intel / AMD可能会发出处理器召回通知! 这也意味着我们不必关心乱序执行等。结果总是保证顺序退出 – 否则我们是塞!

在互锁递增的情况下,处理器需要外出,从给定的地址获取值,然后递增并写回 – 所有这些都是对整个caching行(lockingxadd)的独占所有权,以确保没有其他处理器可以修改它的价值。

随着易失性,你最终仍然只有一个指令(假设JIT是有效的,因为它应该) – inc dword ptr [m_Var]。 然而,处理器(cpuA)并不要求对高速caching行的独占所有权,而是完全使用互锁版本。 可以想象,这意味着其他处理器在被cpuA读取之后可以将更新后的值写回m_Var。 所以不是现在增加了两倍的价值,你最终只有一次。

希望这清除了这个问题。

有关更多信息,请参阅“了解低locking技术在multithreading应用程序中的影响” – http://msdn.microsoft.com/zh-cn/magazine/cc163715.aspx

什么促使这个很晚的答复? 所有的答复在他们的解释中是非常不正确的(特别是标记为答案的答案),我只是为了让其他读者明白这一点。 举重若轻

PPS我假设目标是x86 / x64而不是IA64(它有不同的内存模型)。 请注意,微软的ECMA规范被搞砸了,它指定了最弱的内存模型,而不是最强的内存模型(最好指定最强的内存模型,以便跨平台一致 – 否则将在x86 /虽然英特尔已经为IA64实施了类似强大的内存模型,但是x86可能根本无法在IA64上运行) – 微软自己承认 – http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx

互锁function不locking。 它们是primefaces的,这意味着它们可以在增量过程中没有上下文切换的可能性而完成。 所以不存在僵局或等待的机会。

我会说,你应该总是喜欢它locking和增量。

如果您需要在一个线程中写入另一个线程,并且如果您希望优化程序不对variables重新sorting(因为优化程序不知道另一个线程中发生的事情),则volatile非常有用。 这是一个正确的select,你如何增加。

如果您想了解更多关于无锁代码的信息,以及正确的方法来编写代码,这真是一篇非常好的文章

http://www.ddj.com/hpc-high-performance-computing/210604448

锁(…)工作,但可能会阻塞一个线程,并可能导致死锁,如果其他代码使用相同的锁不兼容的方式。

互锁*是正确的方式来做到这一点…现代CPU支持这个原始的开销less得多。

易变本身是不正确的。 尝试检索并写回修改值的线程仍可能与另一个线程发生冲突。

我是第二个Jon Skeet的回答,并且希望为每个想要了解更多关于“volatile”和Interlocked的人添加以下链接:

primefaces性,波动性和不变性是不同的,第一部分 – (Eric Lippert的“编码中的精彩冒险”(Fabulous Adventures In Coding)

primefaces性,波动性和不变性是不同的,第二部分

primefaces性,波动性和不变性是不同的,第三部分

Sayonara Volatile – (乔·达菲的博客的Wayback Machine快照,2012年出现)

我做了一些testing,看看这个理论是如何工作的: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html 。 我的testing更专注于CompareExchnage,但增量的结果是相似的。 在多CP​​U环境下联锁并不是必须的。 下面是一个2岁16个CPU服务器的增量testing结果。 考虑到testing还涉及到增加后的安全读取,这在现实世界中是典型的。

 D:\>InterlockVsMonitor.exe 16 Using 16 threads: InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial D:\>InterlockVsMonitor.exe 4 Using 4 threads: InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial 

阅读C#参考中的线程 。 它涵盖了你的问题的来龙去脉。 三者各有不同的用途和副作用。