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

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

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

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


当前回答

我赞同Jon Skeet的回答,并想为所有想了解“volatile”和“Interlocked”的人添加以下链接:

原子性,波动性和不可变性是不同的,第一部分- (Eric Lippert的神话般的冒险在编码)

原子性、挥发性和不变性是不同的,第二部分

原子性、挥发性和不变性是不同的,第三部分

Sayonara Volatile - (Wayback Machine 2012年乔·达菲的博客快照)

其他回答

我赞同Jon Skeet的回答,并想为所有想了解“volatile”和“Interlocked”的人添加以下链接:

原子性,波动性和不可变性是不同的,第一部分- (Eric Lippert的神话般的冒险在编码)

原子性、挥发性和不变性是不同的,第二部分

原子性、挥发性和不变性是不同的,第三部分

Sayonara Volatile - (Wayback Machine 2012年乔·达菲的博客快照)

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

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

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

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

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

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

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

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

我做了一些测试来看看这个理论是如何工作的: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

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

将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。