假设一个类有一个public int counter
由多个线程访问的字段.这int
只是递增或递减.
要增加此字段,应使用哪种方法,为什么?
lock(this.locker) this.counter++;
,
Interlocked.Increment(ref this.counter);
,
将访问修饰符更改counter
为public volatile
.
现在,我发现volatile
,我已经删除了许多lock
语句和使用Interlocked
.但是有理由不这样做吗?
将访问修饰符更改
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:
对于任何数量的CORE或CPU,互锁方法都是同时安全的.
互锁方法围绕它们执行的指令应用完整的栅栏,因此不会发生重新排序.
互锁方法不需要甚至不支持访问易失性字段,因为volatile放置在给定字段上的操作周围的半围栏并且互锁使用完整围栏.
由于volatile
不能阻止这些类型的多线程问题,它的用途是什么?一个很好的例子就是说你有两个线程,一个总是写入变量(比如说queueLength
),另一个总是从同一个变量中读取.
如果queueLength
不是volatile,则线程A可以写入五次,但是线程B可能会将这些写入视为延迟(或者甚至可能以错误的顺序).
解决方案是锁定,但在这种情况下你也可以使用volatile.这将确保线程B始终能够看到线程A写入的最新内容.但请注意,只有当你的作家从不读书,读者从不写作,以及你所写的东西是原子价值时,这种逻辑才有效.只要执行单次读取 - 修改 - 写入,就需要进行联锁操作或使用锁定.
编辑:正如在评论中指出,这几天我很高兴地使用Interlocked
了的情况下,单变量的地方是明显没关系.当它变得更复杂时,我仍然会恢复锁定......
volatile
当您需要递增时,使用将无济于事 - 因为读取和写入是单独的指令.另一个线程可能会在您阅读之后但在您回写之前更改该值.
就个人而言,我几乎总是只是锁定 - 以一种显然比波动性或Interlocked.Increment 明显正确的方式更容易正确.就我而言,无锁多线程是真正的线程专家,其中我不是一个.如果Joe Duffy和他的团队构建了一个很好的库,这些库可以在没有像我构建的东西那么多的锁定的情况下进行并行化,这很棒,而且我会在心跳中使用它 - 但是当我自己进行线程处理时,我会尝试把事情简单化.
" volatile
"不会取代Interlocked.Increment
!它只是确保变量不缓存,而是直接使用.
增加变量实际上需要三个操作:
读
增量
写
Interlocked.Increment
将所有三个部分作为单个原子操作执行.
锁定或互锁增量是您正在寻找的.
Volatile绝对不是你想要的 - 它只是告诉编译器将变量视为总是在变化,即使当前代码路径允许编译器优化内存读取.
例如
while (m_Var) { }
如果m_Var在另一个线程中设置为false但它没有被声明为volatile,那么编译器可以自由地使它成为一个无限循环(但并不意味着它总是会)通过对CPU寄存器进行检查(例如EAX因为那是什么m_Var从一开始就被提取)而不是向m_Var的内存位置发出另一个读取(这可能是缓存的 - 我们不知道也不关心,这是x86/x64的缓存一致性点).提到指令重新排序的其他人之前的所有帖子只是表明他们不了解x86/x64架构.挥发性并没有发出读/由早期的帖子说"它可以防止重排"作为暗示写的障碍.实际上,再次感谢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
ps是什么促使这个非常晚的回复?所有的回复都是如此明显不正确(特别是标记为答案的那些)在他们的解释中我只需要清除其他人阅读本文.举重若轻
pps我假设目标是x86/x64而不是IA64(它有不同的内存模型).请注意,Microsoft的ECMA规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好是针对最强的内存模型进行指定,因此它跨平台保持一致 - 否则代码将在x86上运行24-7尽管英特尔已经为IA64实现了类似的强大内存模型,但x64可能无法在IA64上运行 - 微软自己承认了这一点 - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
互锁功能不会锁定.它们是原子的,这意味着它们可以完成而不会在增量期间进行上下文切换.所以没有死锁或等待的可能性.
我会说你应该总是喜欢锁定和增量.
如果您需要在一个线程中写入以在另一个线程中读取,并且您希望优化器不对变量重新排序操作(因为事情发生在优化器不知道的另一个线程中),则Volatile非常有用.这是你如何增量的正交选择.
如果您想了解更多关于无锁代码的信息,以及正确的编写方法,这是一篇非常好的文章
http://www.ddj.com/hpc-high-performance-computing/210604448
lock(...)有效,但可能阻塞一个线程,如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁.
Interlocked.*是正确的方法...因为现代CPU支持它作为原语,所以开销要少得多.
挥发性本身是不正确的.尝试检索然后写回修改值的线程仍然可能与执行相同操作的另一个线程冲突.
我是第二个Jon Skeet的回答,并希望为想要了解更多关于"volatile"和Interlocked的人们添加以下链接:
原子性,波动性和不变性是不同的,第一部分 - (Eric Lippert的编码中的神话般的冒险)
原子性,波动性和不变性是不同的,第二部分
原子性,波动性和不变性是不同的,第三部分
Sayonara Volatile - (2012年出现的Joe Duffy博客的Wayback Machine快照)
我做了一些测试,看看理论是如何运作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html.我的测试更侧重于CompareExchnage,但增量的结果是相似的.在多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