假设一个类有一个公共int计数器字段,可以被多个线程访问。这个int值只能自增或自减。

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

锁(this.locker) this.counter + +; 联锁。增量(ref this.counter); 将counter的访问修饰符更改为public volatile。

现在我已经发现了volatile,我已经删除了许多lock语句和Interlocked的使用。但有理由不这么做吗?


当前回答

我只是在这里指出Orion Edwards回答中关于volatile的错误。

他说:

“如果它是不稳定的,这只是确保两个cpu看到相同的数据 同样的时间。”

这是错误的。在微软关于volatile的文档中,提到:

在多处理器系统上,volatile读操作不会 保证获得写入该内存位置的最新值 任何处理器。类似地,volatile写操作不会 保证写入的值对其他人立即可见 处理器。”

其他回答

我做了一些测试来看看这个理论是如何工作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更侧重于compareexchange,但Increment的结果是类似的。在多cpu环境下,联锁并不需要更快。下面是在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

互锁的函数不锁。它们是原子的,这意味着它们可以在递增过程中不需要上下文切换的情况下完成。因此不存在死锁或等待的机会。

我想说的是,你应该总是更喜欢它,而不是锁和增量。

如果你需要在一个线程中写入,然后在另一个线程中读取,或者你想让优化器不对变量的操作重新排序(因为事情发生在另一个线程中,而优化器不知道),Volatile是有用的。这是一个与增量正交的选择。

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

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

编辑:正如评论中提到的,这些天我很高兴在单个变量的情况下使用Interlocked,这显然是可以的。当它变得更复杂时,我仍然会恢复到锁定…

当你需要增量操作时,使用volatile是没有用的——因为读和写是分开的指令。另一个线程可以在读取后但在回写之前更改值。

就我个人而言,我几乎总是锁定——以一种明显正确的方式获得正确比波动性或interlocking . increment更容易。就我而言,无锁多线程是为真正的线程专家准备的,而我不是。如果Joe Duffy和他的团队构建了很好的库,可以并行化,而不像我构建的那样有太多的锁,那就太棒了,我马上就会使用它——但是当我自己做线程时,我会尽量保持简单。

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

联锁。正确的做法是……更少的开销,因为现代cpu支持这个原语。

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

最坏的情况(实际上不起作用)

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

正如其他人提到的,这本身一点也不安全。volatile的关键在于在多个cpu上运行的多个线程可以缓存数据并重新排序指令。

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

如果它是volatile,这只是确保两个cpu同时看到相同的数据。它根本不会阻止它们的读和写操作交叉,而这正是您试图避免的问题。

第二个最好的:

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

这样做是安全的(前提是您记得锁定访问此.counter的所有其他地方)。它阻止任何其他线程执行任何其他由locker保护的代码。 使用锁还可以防止如上所述的多cpu重新排序问题,这是非常棒的。

问题是,锁的速度很慢,如果你在其他一些不相关的地方重用了这个锁,那么你可能会无缘无故地阻塞其他线程。

Best

联锁。增量(ref this.counter);

这是安全的,因为它有效地在“一次命中”中完成读取、增量和写入,并且不能中断。因此,它不会影响任何其他代码,也不需要记住在其他地方锁定。它也非常快(正如MSDN所说,在现代CPU上,这通常是一个单一的CPU指令)。

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

联锁笔记:

联锁方法在任何数量的内核或cpu上都是同时安全的。 互锁方法在它们执行的指令周围应用了一个完整的围栏,因此不会发生重新排序。 互锁方法不需要甚至不支持对volatile字段的访问,因为volatile在给定字段的操作周围放置了半个围栏,而互锁方法使用了完整的围栏。

脚注:volatile实际上有什么好处。

既然volatile不能防止这些多线程问题,那么它是用来干什么的呢?一个很好的例子是,你有两个线程,一个总是写入一个变量(比如queueLength),另一个总是从同一个变量读取。

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

一种解决方案是锁定,但在这种情况下也可以使用volatile。这将确保线程B始终能够看到线程A所写的最新内容。但是请注意,这种逻辑只适用于以下情况:作者从不阅读,读者从不写作,并且您正在编写的内容是一个原子值。一旦执行了单个的read-modify-write操作,就需要转到Interlocked操作或使用Lock。