当前位置:  开发笔记 > 编程语言 > 正文

易失性与联锁对抗锁定

如何解决《易失性与联锁对抗锁定》经验,为你挑选了8个好方法。

假设一个类有一个public int counter由多个线程访问的字段.这int只是递增或递减.

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

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

Interlocked.Increment(ref this.counter);,

将访问修饰符更改counterpublic volatile.

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



1> Orion Edward..:

最糟糕的(实际上不会起作用)

将访问修饰符更改counterpublic 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写入的最新内容.但请注意,只有当你的作家从不读书,读者从不写作,以及你所写的东西是原子价值时,这种逻辑有效.只要执行单次读取 - 修改 - 写入,就需要进行联锁操作或使用锁定.


太多了!关于"挥发性实际上有什么好处"的脚注是我正在寻找并确认我想如何使用volatile.
@Zach Saw:在C++的内存模型下,volatile就是你如何描述的(基本上对设备映射的内存很有用,而不是很多).在*CLR*的内存模型下(此问题标记为C#),volatile会在读取和写入该存储位置时插入内存屏障.你告诉*处理器*不要重新排序东西,而且它们非常重要......你记忆障碍(以及一些装配指令的特殊锁定变化)
"我不完全确定......如果你还需要将波动与增量结合起来." 它们不能合并AFAIK,因为我们不能通过ref传递volatile.顺便说一下很棒的答案.
@ZachSaw:C#中的volatile字段阻止C#编译器和jit编译器进行某些优化以缓存该值.它还可以确保在多个线程上可以观察到读取和写入的顺序.作为实现细节,它可以通过在读取和写入上引入内存屏障来实现.保证的精确语义在规范中描述; 请注意,规范确实*不*保证*all*threads的*all*volatile写入和读取的顺序将由*all*threads观察到.
换句话说,如果var被声明为volatile,编译器将假定每次代码遇到var时,var的值都不会保持不变(即volatile).因此,在一个循环中,例如:while(m_Var){},并且m_Var在另一个线程中设置为false,编译器不会简单地检查先前加载了m_Var值的寄存器中的内容,但是从m_Var读取值再次.但是,这并不意味着不声明volatile会导致循环无限地继续 - 指定volatile只保证如果m_Var在另一个线程中设置为false则不会.
@ZachSaw - 重新阅读很久以前的评论,我意识到我的评论是错误的 - *"在CLR的内存模型下,volatile会在读取和写入存储位置时插入内存障碍"* - CLR将为ARM*上的一些易失操作*插入内存屏障,因为它的内存模型较弱 - 但你是对的,它不会为x86执行此操作,因为它不需要.我很抱歉
@Zach锯:虽然你说得对,对大多数多CPU系统的这些天,高速缓存一致性将保持每个CPU的数据是最新的,它*不会*账户的事实,CPU的可以(做)重新订购说明.你需要内存屏障来防止这种情况,正如我上面提到的,CLR中的volatile会在每次读/写易失性存储位置时发出内存障碍......
+1:CLR通过C#有一个关于volatile的部分,归结为精确到此.volatile解决的情况称为缓存一致性(http://en.wikipedia.org/wiki/Cache_coherence).
@ZachSaw,对不起,但你有点不对劲.查看专家的这篇文章:http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx - 在C#中, "volatile"不仅意味着"确保编译器和抖动不对此变量执行任何代码重新排序或寄存器缓存优化".它还意味着"告诉处理器做他们需要做的任何事情,以确保我正在读取最新的值,即使这意味着停止其他处理器并使它们与主存储器的缓存同步".
@zihotki:这是简单的版本.看看评论 - 作者被问到有关缓存一致性的具体问题,他的回答是我一直试图解释的,显然对某些人无济于事......
@ZachSaw:是的:规范说获取和释放语义将强加于易失性读写.运行时选择如何做到这一点取决于它; 如果由于特定处理器做出的某些其他保证而导致*不*引入完整或半栅栏*指令*,则它没有义务生成不必要的代码.
@ZachSaw感叹..我毫不怀疑你比我更了解一致性和重新排序,但你似乎错过了这个问题是C#问题,所以我们谈论的是*CLR*内存模型,不是x86/64,或任何其他内存模型
@EricLippert是的.这正是我一直试图解释的.很高兴看到大家终于赶上了!
@JohnTaylor对,是的.我检查了CLR JIT生成的程序集,它发出了一条后增量的指令,所以优化器显然已经得到了它.无论如何,**inc [ebx + 0x12]**仍然涉及CPU进行内存读取,增量和内存写入

2> Jon Skeet..:

编辑:正如在评论中指出,这几天我很高兴地使用Interlocked了的情况下,单变量的地方是明显没关系.当它变得更复杂时,我仍然会恢复锁定......

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

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


+1,以确保我从现在开始忘记无锁编码.
@ZachSaw:你的第二条评论说联锁操作在某个阶段"锁定"; 术语"锁定"通常意味着一个任务可以在无限长的时间内保持对资源的独占控制; 无锁编程的主要优点是它避免了资源由于拥有任务变得无法使用而导致无法使用的危险.互锁类使用的总线同步不仅"通常更快" - 在大多数系统上它具有有限的最坏情况时间,而锁定则没有.
无锁代码肯定不是真正的无锁,因为它们锁定在某个阶段 - 无论是在(FSB)总线还是在interCPU级别,你仍然需要付出代价.但是,只要不使锁定发生的带宽饱和,锁定在这些较低级别通常会更快.
@Jaap:是的,这些天我会*使用互锁的真正的单一计数器.我只是不想开始搞乱尝试解决*multiple*lock-free更新变量之间的交互.
Interlocked没有任何问题,它正是你所寻找的并且比完全锁更快()
如果你说"无锁多线程是真正的线程专家,我不是其中之一." 我永远不会尝试编写无锁多线程代码.:)

3> Michael Dama..:

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

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

    增量

Interlocked.Increment 将所有三个部分作为单个原子操作执行.


换句话说,互锁的变化是完全围栏的,因此是原子的.挥发性成员仅部分围栏,因此不保证是线程安全的.

4> Zach Saw..:

锁定或互锁增量是您正在寻找的.

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.


是的,你所说的一切,如果不是100%至少99%的商标.当你在工作中匆忙开发时,这个网站(大部分)非常有用,但不幸的是,与(游戏)投票相对应的答案的准确性并不存在.所以基本上在stackoverflow中你可以感受到读者的流行理解,而不是它的真正含义.有时,最重要的答案只是纯粹的胡言乱语 - 善意的神话.不幸的是,这是在解决问题时遇到阅读的人们的滋生.但这是可以理解的,没有人能够知道一切.
有趣.你能参考一下吗?我很高兴地投票支持这一点,但是在与我所阅读的资源一致的高度投票答案3年后用一些激进的语言发布,需要更加切实的证据.
为什么有人想阻止CPU缓存超出我的范围.专用于执行缓存一致性的整个房地产(绝对不可忽略的大小和成本)是完全浪费的,如果是这样的话......除非你不需要缓存一致性,比如显卡,PCI设备等,你就不会设置直写的缓存行.

5> Lou Franco..:

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

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

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

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

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



6> Rob Walker..:

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

Interlocked.*是正确的方法...因为现代CPU支持它作为原语,所以开销要少得多.

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



7> zihotki..:

我是第二个Jon Skeet的回答,并希望为想要了解更多关于"volatile"和Interlocked的人们添加以下链接:

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

原子性,波动性和不变性是不同的,第二部分

原子性,波动性和不变性是不同的,第三部分

Sayonara Volatile - (2012年出现的Joe Duffy博客的Wayback Machine快照)



8> Kenneth Xu..:

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


再看一遍.如果真正的瓶颈在于FSB,则监视器实现应该遵循相同的瓶颈.真正的区别在于Interlocked正在进行繁忙的等待和重试,这成为高性能计数的真正问题.至少我希望我的评论引起人们的注意,Interlocked并不总是正确的计算选择.人们正在寻找替代品的事实很好地解释了它.你需要一个长加法器http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/LongAdder.html
推荐阅读
jerry613
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有