易失性与连锁性与锁定

时间:2020-03-06 14:56:31  来源:igfitidea点击:

假设一个类具有一个"公共整数计数器"字段,该字段可被多个线程访问。这个int只会增加或者减少。

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

  • lock(this.locker)this.counter ++;
  • Interlocked.Increment(ref this.counter);
  • 将"计数器"的访问修饰符更改为"公共易失性"。

既然我已经发现了'volatile',那么我已经删除了许多`lock'语句以及使用了" Interlocked"。但是,有理由不这样做吗?

解决方案

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

我要说的是,我们应该始终偏爱锁定和递增操作。

如果我们需要在一个线程中进行写入以在另一个线程中进行读取,并且如果我们希望优化程序不对变量进行重新排序(因为在优化程序不知道的另一个线程中发生的事情),则Volatile很有用。这是增加方式的正交选择。

如果我们想了解有关无锁代码的更多信息以及正确的编写方式,那么这是一篇非常不错的文章。

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

编辑:正如评论中指出的那样,这些天来,我很乐意在单个变量明显可用的情况下使用"互锁"。当事情变得更加复杂时,我仍将恢复锁定状态。

当需要递增时,使用'volatile'将无济于事,因为读和写是分开的指令。读完之后但写回之前,另一个线程可能会更改该值。

就我个人而言,几乎总是锁定它,以明显正确的方式比波动或者Interlocked.Increment容易。就我而言,无锁多线程是针对真正的线程专家的,我不是其中之一。如果Joe Duffy和他的团队构建了不错的库,这些库可以并行化事物而没有我要构建的东西那么多,那真是太好了,我将在心跳中使用它,但是当我自己进行线程化时,我会尽量保持很简单。

lock(...)可以工作,但是可能会阻塞线程,并且如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。

Interlocked。*是执行此操作的正确方法...开销要少得多,因为现代CPU支持将此作为原始函数。

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

" volatile"不能代替Interlocked.Increment!它只是确保该变量不被缓存,而是直接使用。

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

  • 增量

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

阅读Creference中的线程。它涵盖了我们问题的来龙去脉。这三个都有不同的用途和副作用。

最差(实际上不会工作)

Change the access modifier of counter to public volatile

正如其他人所提到的那样,仅此一点实际上是不安全的。 volatile的意义在于,运行在多个CPU上的多个线程可以并且将缓存数据并重新排序指令。

如果它不是"易失性"的,并且CPU A增加了一个值,则CPU B可能直到一段时间后才能真正看到该增加的值,这可能会引起问题。

如果它是易失性的,那只能确保两个CPU同时看到相同的数据。它根本不会阻止他们交错读取和写入操作,而这正是我们要避免的问题。

次好的:

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

这是安全的(假设我们记得在访问this.counter的其他任何地方都将其锁定)。它可以防止其他任何线程执行由`locker'保护的任何其他代码。
同样,使用锁可以防止上述多CPU重新排序问题,这非常好。

问题是,锁定很慢,如果我们在与实际无关的其他地方重新使用" locker",则最终可能会无缘无故地阻塞其他线程。

最好

Interlocked.Increment(ref this.counter);

这是安全的,因为它可以有效地读取,递增和写入不会中断的"一次命中"。因此,它不会影响任何其他代码,并且我们也不必记住锁定其他任何位置。它也非常快(如MSDN所说,在现代CPU上,这实际上是一条CPU指令)。

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

连锁注意事项:

  • 互锁方法可同时在任意数量的内核或者CPU上使用。
  • 互锁的方法在执行的指令周围加上了完整的围栏,因此不会发生重新排序。
  • 互锁方法不需要甚至不支持访问易失性字段,因为在给定字段上的操作周围将易失性放置了半围墙,而互锁使用的是全围墙。

脚注:挥发物实际上是有益的。

由于`volatile'不能防止此类多线程问题,它的用途是什么?一个很好的例子是说我们有两个线程,一个线程总是写一个变量(比如说" queueLength"),而另一个线程总是从同一个变量中读取。

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

解决方案是锁定,但在这种情况下也可以使用volatile。这样可以确保线程B始终可以看到线程A编写的最新内容。但是请注意,只有当我们有从未读过的作家和从未写过的读者,并且我们要写的东西是原子值时,此逻辑才起作用。一旦完成一次读-修改-写操作,就需要进入"互锁"操作或者使用"锁定"。

我做了一些测试以了解该理论的实际工作原理:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更多地侧重于CompareExchnage,但是Increment的结果却相似。在多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