易失性与联锁对抗锁定

问题描述 投票:625回答:9

假设一个类有一个由多个线程访问的public int counter字段。此int仅递增或递减。

要增加此字段,应使用哪种方法,为什么?

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

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

c# multithreading locking volatile interlocked
9个回答
821
投票

Worst (won't actually work)

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

正如其他人所提到的,这本身并不是真正的安全。 volatile的观点是,在多个CPU上运行的多个线程可以缓存数据并重新排序指令。

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

如果它是volatile,这只是确保两个CPU同时看到相同的数据。它根本不会阻止它们进行读写操作,这是你试图避免的问题。

Second Best:

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

这是安全的(如果你记得你访问lock的其他地方的this.counter)。它可以防止任何其他线程执行由locker保护的任何其他代码。使用锁也可以防止上面的多CPU重新排序问题,这很好。

问题是,锁定很慢,如果你在其他一些与真正无关的地方重新使用locker,那么你最终可能无缘无故地阻止你的其他线程。

Best

Interlocked.Increment(ref this.counter);

这是安全的,因为它有效地执行“一次点击”中的读取,递增和写入,这是无法中断的。因此,它不会影响任何其他代码,也不需要记住锁定其他地方。它也非常快(正如MSDN所说,在现代CPU上,这通常只是一条CPU指令)。

我不完全确定它是否绕过其他CPU重新排序,或者你是否还需要将volatile与增量结合起来。

InterlockedNotes:

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

Footnote: What volatile is actually good for.

由于volatile不能阻止这些类型的多线程问题,它有什么用呢?一个很好的例子就是说你有两个线程,一个总是写入变量(比如queueLength),另一个总是从同一个变量读取。

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

解决方案是锁定,但在这种情况下你也可以使用volatile。这将确保线程B始终能够看到线程A写入的最新内容。但请注意,只有当你的作家从不读书,读者从不写作,以及你所写的东西是原子价值时,这种逻辑才有效。只要执行单次读取 - 修改 - 写入,就需要进行联锁操作或使用锁定。


135
投票

编辑:正如评论中所述,这些天我很高兴使用Interlocked作为单个变量的情况,显然没问题。当它变得更复杂时,我仍然会恢复锁定......

当你需要递增时,使用volatile无济于事 - 因为读和写是单独的指令。另一个线程可能会在您阅读之后但在您回写之前更改该值。

就个人而言,我几乎总是只是锁定 - 以一种显然比波动性或Interlocked.Increment明显正确的方式更容易正确。就我而言,无锁多线程是真正的线程专家,其中我不是一个。如果Joe Duffy和他的团队构建了一个很好的库,这些库可以在没有像我构建的东西那么多的锁定的情况下进行并行化,这很棒,而且我会在心跳中使用它 - 但是当我自己进行线程处理时,我会尝试把事情简单化。


42
投票

volatile”不会取代Interlocked.Increment!它只是确保变量不缓存,而是直接使用。

增加变量实际上需要三个操作:

  1. 增量

Interlocked.Increment将所有三个部分作为单个原子操作执行。


41
投票

锁定或互锁增量是您正在寻找的。

Volatile绝对不是你想要的 - 它只是告诉编译器将变量视为总是在变化,即使当前代码路径允许编译器优化内存读取。

EG

while (m_Var)
{ }

如果m_Var在另一个线程中设置为false但它没有被声明为volatile,那么编译器可以自由地使它成为一个无限循环(但并不意味着它总是会)通过对CPU寄存器进行检查(例如EAX因为那是什么m_Var从一开始就被提取)而不是向m_Var的内存位置发出另一个读取(这可能是缓存的 - 我们不知道也不关心,这是x86 / x64的缓存一致性点)。提到指令重新排序的其他人之前的所有帖子只是表明他们不了解x86 / x64架构。 Volatile不会发出读/写障碍,正如之前的帖子暗示的那样“它可以防止重新排序”。实际上,再次感谢MESI协议,我们保证无论实际结果是退回到物理内存还是只是驻留在本地CPU的缓存中,我们读取的结果在CPU之间始终是相同的。我不会对此细节进行太深入的讨论,但请放心,如果出现这种情况,英特尔/ AMD可能会召回处理器!这也意味着我们不必关心乱序执行等。结果总是保证按顺序退出 - 否则我们就被塞满了!

使用Interlocked Increment,处理器需要熄灭,从给定的地址中获取值,然后递增并将其写回 - 所有这些都拥有整个缓存行的独占所有权(锁定xadd),以确保没有其他处理器可以修改它的价值。

对于volatile,你最终只会得到1条指令(假设JIT是有效的) - inc dword ptr [m_Var]。但是,处理器(cpuA)在完成对互锁版本的所有操作时不会要求对缓存行进行独占所有权。可以想象,这意味着其他处理器可以在cpuA读取后将更新后的值写回m_Var。因此,现在不是将值增加两倍,而是仅使用一次。

希望这能解决问题。

有关详细信息,请参阅“了解多线程应用程序中低锁技术的影响” - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

附:是什么促使这个非常晚的回复?所有的回复都是如此明显不正确(特别是标记为答案的那些)在他们的解释中我只需要清除其他人阅读本文。举重若轻

p.p.s.我假设目标是x86 / x64而不是IA64(它有不同的内存模型)。请注意,Microsoft的ECMA规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好指定最强的内存模型,因此它在各个平台上都是一致的 - 否则代码将在x86上运行24-7尽管英特尔已经为IA64实施了类似的强大内存模型,但x64可能根本不会在IA64上运行 - 微软自己承认了这一点 - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx


15
投票

互锁功能不会锁定。它们是原子的,这意味着它们可以完成而不会在增量期间进行上下文切换。所以没有死锁或等待的可能性。

我会说你应该总是喜欢锁定和增量。

如果您需要在一个线程中写入以在另一个线程中读取,并且您希望优化器不对变量重新排序操作(因为事情发生在优化器不知道的另一个线程中),则Volatile非常有用。这是你如何增量的正交选择。

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

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


11
投票

lock(...)有效,但可能阻塞一个线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。

Interlocked。*是正确的方法...因为现代CPU支持它作为原语,所以开销要少得多。

挥发性本身是不正确的。尝试检索然后写回修改值的线程仍然可能与执行相同操作的另一个线程冲突。



7
投票

我做了一些测试,看看这个理论是如何运作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更侧重于CompareExchnage,但增量的结果是相似的。在多CP​​U环境中,互锁不是更快。以下是2年16 CPU服务器上的Increment的测试结果。请记住,测试还涉及增加后的安全读取,这在现实世界中是典型的。

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

2
投票

我想在其他答案中添加volatileInterlockedlock之间的区别:

The volatile keyword can be applied to fields of these types

  • 参考类型。
  • 指针类型(在不安全的上下文中)。请注意,虽然指针本身可以是易失性的,但它指向的对象却不能。换句话说,您不能将“指针”声明为“volatile”。
  • 简单的类型,如sbytebyteshortushortintuintcharfloatbool
  • 具有以下基本类型之一的枚举类型:bytesbyteshort,ushort,intuint
  • 已知为通用类型参数的引用类型。
  • IntPtrUIntPtr

其他类型,包括doublelong,不能标记为“volatile”,因为对这些类型的字段的读取和写入不能保证是原子的。要保护对这些类型字段的多线程访问,请使用Interlocked类成员或使用lock语句保护访问权限。

© www.soinside.com 2019 - 2024. All rights reserved.