Interlocked.CompareExchange真的比简单的锁更快吗?

And*_*ena 6 .net c# multithreading interlocked thread-safety

我遇到了ConcurrentDictionary.NET 3.5 的实现(我很抱歉,我现在可以找到链接),它使用这种方法进行锁定:

var current = Thread.CurrentThread.ManagedThreadId;
while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }

// PROCESS SOMETHING HERE

if (current != Interlocked.Exchange(ref owner, 0))
        throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");
Run Code Online (Sandbox Code Playgroud)

而不是传统的lock:

lock(lockObject)
{
    // PROCESS SOMETHING HERE
}
Run Code Online (Sandbox Code Playgroud)

问题是:这样做有什么真正的理由吗?它更快还是有一些隐藏的好处?

PS:我知道有ConcurrentDictionary一些最新版本的.NET,但我不能用于遗留项目.

编辑:

在我的具体情况下,我正在做的只是Dictionary以一种线程安全的方式操作内部类.

例:

public bool RemoveItem(TKey key)
{
    // open lock
    var current = Thread.CurrentThread.ManagedThreadId;
    while (Interlocked.CompareExchange(ref owner, current, 0) != current) { }


    // real processing starts here (entries is a regular `Dictionary` class.
    var found = entries.Remove(key);


    // verify lock
    if (current != Interlocked.Exchange(ref owner, 0))
        throw new UnauthorizedAccessException("Thread had access to cache even though it shouldn't have.");
    return found;
}
Run Code Online (Sandbox Code Playgroud)

正如@doctorlove建议的那样,这是代码:https://github.com/miensol/SimpleConfigSections/blob/master/SimpleConfigSections/Cache.cs

Joe*_*Joe 6

如果"PROCESS SOMETHING HERE"抛出异常,则CompareExchange示例代码不会释放锁定.

出于这个原因以及更简单,更易读的代码,我更喜欢lock语句.

你可以用try/finally来纠正这个问题,但这会使代码变得更加丑陋.

链接的ConcurrentDictionary实现有一个错误:它会失败,如果调用者传递一个空键,潜在地使其他线程纺无限期地解除锁定.

至于效率,您的CompareExchange版本本质上是一个Spinlock,如果线程很可能在短时间内被阻塞,它可以很有效.但是插入托管字典可能需要相当长的时间,因为可能需要调整字典的大小.因此,恕我直言,这不是一个好的螺旋锁候选者 - 这可能是浪费,特别是在单处理器系统上.


ken*_*n2k 6

你的问题没有明确的答案.我会回答:这取决于.

你提供的代码是:

  1. 等待对象处于已知状态(threadId == 0 == no current work)
  2. 做工作
  3. 将已知状态设置回对象
  4. 另一个线程现在也可以工作,因为它可以从第1步到第2步

正如您所指出的,您在代码中有一个实际执行"等待"步骤的循环.在您可以访问关键部分之前,不要阻止该线程,而只是刻录CPU.尝试替换你的处理(在你的情况下,调用Remove)Thread.Sleep(2000),你会看到另一个"等待"线程在循环中占用你的所有一个CPU 2s.

这意味着哪一个更好取决于几个因素.例如:有多少并发访问?操作需要多长时间才能完成?你有多少CPU?

我会使用lock而不是Interlocked因为它更容易阅读和维护.例外的情况是,您有一段数百万次的代码,而且您确定的特定用例Interlocked更快.

所以你必须自己衡量两种方法.如果你没有时间,那么你可能不需要担心表演,你应该使用lock.


Eri*_*let 5

有点晚了......我已经阅读了你的示例,但简而言之:

最快到最慢的 MT 同步:

  • Interlocked.* => 这是一条 CPU 原子指令。如果它足以满足您的需求,那么它是无可匹敌的。
  • SpinLock => 使用后面的互锁并且速度非常快。等待时使用CPU。不要用于等待时间较长的代码(通常用于防止执行快速操作的锁的线程切换)。如果你经常需要等待多个线程周期,我建议使用“Lock”
  • Lock => 比 SpinLock 最慢但更易于使用和读取。该指令本身非常快,但如果它无法获取锁,它将放弃 CPU。在幕后,它将在内核对象 (CriticalSection) 上执行 WaitForSingleObject,然后仅当获取锁的线程释放锁时,Window 才会为线程提供 cpu 时间。

和MT一起玩吧!


i3a*_*non 3

是的。该类Interlocked提供原子操作,这意味着它们不会像锁一样阻塞其他代码,因为它们实际上并不需要这样做。当您锁定一段代码时,您需要确保没有 2 个线程同时位于其中,这意味着当一个线程位于其中时,所有其他线程都会等待进入,这会占用资源(CPU 时间和空闲线程)。另一方面,原子操作不需要阻止其他原子操作,因为它们是原子的。从概念上讲,它是一个 CPU 操作,下一个操作会在前一个操作之后进入,并且您不会在等待上浪费线程。(顺便说一句,这就是为什么它仅限于非常基本的操作Increment,例如Exchange等)

我认为锁(下面是一个监视器)使用互锁来知道锁是否已经被占用,但它不知道它内部的操作可以是原子的。

但在大多数情况下,差异并不重要。但您需要针对您的具体情况进行验证。