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

时间:2013-12-27 12:44:13

标签: c# .net multithreading thread-safety interlocked

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

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.");

而不是传统的lock

lock(lockObject)
{
    // PROCESS SOMETHING HERE
}

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

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

修改

在我的具体情况中,我正在做的只是以一种线程安全的方式操作内部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;
}

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

7 个答案:

答案 0 :(得分:6)

如果“PROCESS SOMETHING HERE”引发异常,您的CompareExchange示例代码不会释放锁定。

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

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

linked ConcurrentDictionary implementation有一个错误:如果调用者传递一个空键,它将无法释放锁定,可能会使其他线程无限期地旋转。

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

答案 1 :(得分:6)

您的问题没有明确的答案。我会回答:取决于

您提供的代码是:

  1. 等待对象处于已知状态(threadId == 0 == no current work
  2. 做好工作
  3. 将已知状态设置为对象
  4. 另一个线程现在也可以工作,因为它可以从第1步到第2步
  5. 正如您所指出的,您在代码中有一个实际执行“等待”步骤的循环。 您不能阻止该线程,直到您可以访问您的关键部分,只是刻录CPU 。尝试用Remove替换你的处理(在你的情况下,调用Thread.Sleep(2000)),你会看到另一个“等待”线程在循环中占用你所有的一个CPU 2s。 / p>

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

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

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

答案 2 :(得分:3)

是。 Interlocked类提供原子操作,这意味着它们不会像锁一样阻止其他代码,因为它们并不真正需要。 当您锁定一个代码块时,您要确保同时没有2个线程,这意味着当一个线程在所有其他线程内等待进入时,它会使用资源( cpu时间和空闲线程)。 另一方面,原子操作不需要阻止其他原子操作,因为它们是原子的。它在概念上是一个CPU操作,下一个只是在前一个之后进入,并且你不会在等待时浪费线程。 (顺便说一句,这就是为什么它仅限于IncrementExchange等非常基本的操作。)

我认为一个锁(它下面是一个监视器)使用了互锁来知道锁是否已被占用,但它无法知道其中的动作可能是原子的。

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

答案 3 :(得分:3)

Interlocked课程的docs告诉我们

“为多个线程共享的变量提供原子操作。”

理论是原子操作可以比锁更快。 Albahari提供了有关互锁操作的更多详细信息,说明它们更快。

请注意,Interlocked提供的“小”界面不是Lock - 请参阅上一个问题here

答案 4 :(得分:1)

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

速度最慢的MT同步:

  • 互锁。* =>这是一个CPU原子指令。如果足以满足您的需求,就不会被打败。
  • SpinLock =>使用Interlocked背后,非常快。等待时使用CPU。不要用于等待很长时间的代码(它通常用于防止线程切换以进行快速操作的锁定)。如果您经常需要等待多个线程周期,我建议您使用" Lock"
  • Lock =>比SpinLock最慢但更容易使用和阅读。指令本身非常快,但如果它无法获得锁定,它将放弃cpu。在场景后面,它将在内核对象(CriticalSection)上执行WaitForSingleObject,然后只有当锁获取它的线程释放锁时,Window才会给线程提供cpu时间。

与MT玩得开心!

答案 5 :(得分:1)

lock 和 interlock.CompareExhange 之间的一个重要区别是它如何在异步环境中使用。

异步操作不能在锁内等待,因为如果等待后继续执行的线程与最初获取锁的线程不同,它们很容易发生死锁。

这不是互锁的问题,因为线程没有“获取”任何东西。

另一种可能提供比互锁更好的可读性的异步代码解决方案可能是信号量,如本博客文章中所述: https://blog.cdemi.io/async-waiting-inside-c-sharp-locks/

答案 6 :(得分:0)

互锁更快-在其他注释中已经说明,您还可以定义如何实现等待的逻辑,例如一旦锁第一次失败,则spinWait.spin(),spinUntil,Thread.sleep等。此外,如果希望锁中的代码运行而不会崩溃(自定义代码/代理/资源解析或分配/事件/锁定期间执行了意外的代码),除非您要捕获异常以允许您的软件继续执行,否则也将跳过“尝试”“最终”操作,从而提高了速度。锁定(某物)可确保您是否从外部捕获异常以解锁某物,就像“使用”可确保执行(无论出于何种原因要退出“使用”的一次性对象)退出执行块时(C#)一样。