易失性与联锁对抗锁定

时间:2008-09-30 19:25:07

标签: c# multithreading locking volatile interlocked

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

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

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

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

9 个答案:

答案 0 :(得分:807)

最差(实际上不会工作)

  

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

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

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

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

第二好:

  

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

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

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

最佳

  

Interlocked.Increment(ref this.counter);

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

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

InterlockedNotes:

  1. 联锁方法可以同时保护任何数量的核心或CPU。
  2. 互锁方法围绕它们执行的指令应用完整的栅栏,因此不会重新排序。
  3. 互锁方法不需要甚至不支持访问易失性字段,因为volatile会在给定字段的操作周围放置半围栏并且互锁使用完整围栏。
  4. 脚注:什么挥发物实际上有益。

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

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

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

答案 1 :(得分:135)

编辑:正如评论中所述,这些天我很高兴将Interlocked用于单变量的情况,其中显然是 好的。当它变得更复杂时,我仍然会恢复锁定......

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

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

答案 2 :(得分:42)

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

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

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

答案 3 :(得分:41)

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

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

e.g。

while (m_Var)
{ }

如果m_Var在另一个线程中被设置为false但它没有被声明为volatile,那么编译器可以通过对CPU寄存器进行检查来自由地使它成为无限循环(但并不意味着它总是会这样)(例如EAX)因为这就是m_Var从一开始就进入的内容)而不是向m_Var的内存位置发出另一个读取(这可能是缓存的 - 我们不知道也不关心,这就是x86 /的缓存一致性点64)。提到指令重新排序的其他人之前的所有帖子只是表明他们不了解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.S。是什么促使这个非常晚的回复?所有的回复都是如此明显不正确(特别是标记为答案的那些)在他们的解释中我只需要清除其他人阅读本文。 耸肩

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

答案 4 :(得分:15)

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

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

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

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

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

答案 5 :(得分:11)

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

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

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

答案 6 :(得分:8)

答案 7 :(得分: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

答案 8 :(得分:2)

我想补充一下其他答案中提到的volatileInterlockedlock之间的区别:

The volatile keyword can be applied to fields of these types

  • 引用类型。
  • 指针类型(在不安全的上下文中)。请注意,尽管指针本身可以是易失性的,但其指向的对象却不能。其他 的话,您不能将“指针”声明为“易失性”。
  • 简单类型,例如sbytebyteshortushortintuintchar,{ {1}}和float
  • 具有以下基本类型之一的枚举类型:boolbytesbyte,ushort,shortint
  • 已知为引用类型的通用类型参数。
  • uintIntPtr

其他类型,包括UIntPtrdouble,不能标记为“易失性” 因为无法保证对这些类型的字段的读写 是原子的。为了保护对这些类型的多线程访问 字段中,使用long类成员,或使用 Interlocked声明。