首页 文章

易失性与联锁对抗锁定

提问于
浏览
605

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

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

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

  • Interlocked.Increment(ref this.counter);

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

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

10 回答

  • 1

    最差(实际上不会起作用)

    将计数器的访问修饰符更改为公共volatile

    正如其他人所提到的,这本身并不是真正的安全 . volatile 的意思是多个CPU上运行的多个线程可以缓存数据并重新排序指令 .

    如果它是 not 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 .

    • 互锁方法在它们执行的指令周围应用完整的栅栏,因此不会发生重新排序 .

    • 互锁方法 do not need or even do not support access to a volatile field ,因为volatile被放置在给定字段上的操作周围的半栅栏并且互锁使用完整栅栏 .

    脚注:什么挥发物实际上有益 .

    因为 volatile 不是吗?一个很好的例子就是说你有两个线程,一个总是写入变量(比如 queueLength ),另一个总是从同一个变量中读取 .

    如果 queueLength 不是易失性的,则线程A可能会写入五次,但是线程B可能会将这些写入视为被延迟(或者甚至可能以错误的顺序) .

    解决方案是锁定,但在这种情况下你也可以使用volatile . 这将确保线程B始终能够看到线程A写入的最新内容 . 但请注意,只有当你的作家从不读书,读者从不写作,以及你所写的东西是原子 Value 时,这种逻辑才有效 . 只要执行单次读取 - 修改 - 写入,就需要进行联锁操作或使用锁定 .

  • 8

    EDIT: 如评论中所述,这些天我很乐意将 Interlocked 用于单个变量的情况,显然这是正常的 . 当它变得更复杂时,我仍然会恢复锁定......

    使用 volatile 赢了't help when you need to increment - because the read and the write are separate instructions. Another thread could change the value after you'已读但在回写之前 .

    就个人而言,我几乎总是只是锁定 - 以一种显然比波动性或Interlocked.Increment明显正确的方式更容易正确 . 至于我'm concerned, lock-free multi-threading is for real threading experts, of which I'米不是一个 . 如果Joe Duffy和他的团队构建了一个很好的库,这些库可以在没有锁定的情况下实现并行化,而且我自己也可以使用线程,我会尽量保持简单 .

  • 7

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

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

    • 阅读

    • 增量

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

  • 41

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

    挥发物绝对不是你想要的 - 它只是告诉编译器将变量视为总是在改变,即使当前代码路径允许编译器优化内存读取 .

    例如

    while (m_Var)
    { }
    

    如果m_Var在另一个线程中被设置为false但是它总是会意味着它通过对CPU寄存器进行检查(例如EAX,因为那是m_Var从一开始就被读取)而不是向内存发出另一个读取m_Var的位置(这可能是缓存的 - 我们并不关心x14 / x64体系结构 . 挥发性不会发出读/写障碍,正如之前的帖子所暗示的那样'it prevents reordering' . 事实上,再次感谢MESI协议,无论实际结果是否已经退回到物理内存,或者只是驻留在本地CPU中,我们保证我们读取的结果始终是相同的's cache. I won' t太详细了但是请放心,如果这出错了,英特尔/ AMD可能会发出处理器召回事件!这也意味着我们不必关心乱序执行等 . 结果总是保证按顺序退出 - 否则我们就被塞满了!

    使用Interlocked Increment,处理器需要熄灭,从给定的地址中获取值,然后递增并将其写回 - 所有这些都拥有整个缓存行的独占所有权(锁定xadd),以确保没有其他处理器可以修改它的 Value .

    对于volatile,你最终只会得到1条指令(假设JIT是有效的) - inc dword ptr [m_Var] . 但是,处理器(cpuA)在完成对互锁版本的所有操作时不会要求对缓存行进行独占所有权 . 可以想象,这意味着其他处理器可以在cpuA读取后将更新后的值写回m_Var . 因此,现在不是将值增加两倍,而是仅使用一次 .

    希望这能解决问题 .

    有关详细信息,请参阅'Understand the Impact of Low-Lock Techniques in Multithreaded Apps' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx

    附:是什么促使这个非常晚的回复?所有的回复都是如此明显不正确(特别是标记为答案的那些)在他们的解释中我只需要清除其他人阅读本文 . 举重若轻

    p.p.s.我已经搞砸了ECMA规范,因为它指定了最弱的内存模型而不是最强的内存模型(最好是针对最强的内存模型进行指定,因此它在各个平台上都是一致的 - 否则代码将在x86上运行24-7尽管英特尔已经为IA64实施了类似的强大内存模型,但x64可能根本不会在IA64上运行 - 微软自己承认了这一点 - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .

  • 4

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

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

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

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

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

  • 11

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

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

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

  • 15

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

    阅读Threading in C#参考 . 它涵盖了您问题的细节 . 三者中的每一个都有不同的目的和方面效果 .

  • 129

    我想在其他答案中添加volatile,Interlocked和lock之间的区别:

    volatile关键字可以应用于这些类型的字段:引用类型 . 指针类型(在不安全的上下文中) . 请注意,虽然指针本身可以是易失性的,但它指向的对象却不能 . 换句话说,您不能声明“指向volatile的指针” . 简单类型,如sbyte,byte,short,ushort,int,uint,char,float和bool . 具有以下基类型之一的枚举类型:byte,sbyte,short,ushort,int或uint . 已知为通用类型参数的引用类型 . IntPtr和UIntPtr . 其他类型(包括double和long)不能标记为volatile,因为对这些类型的字段的读取和写入不能保证是原子的 . 要保护对这些类型字段的多线程访问,请使用Interlocked类成员或使用lock语句保护访问 .

相关问题