旋锁,它们有用吗?

时间:2009-09-21 18:59:48

标签: c# c++ multithreading locking spinlock

您经常在代码中发现自己使用自旋锁吗?遇到使用繁忙循环的情况实际上优于锁的使用情况有多常见? 就个人而言,当我编写某种需要线程安全的代码时,我倾向于使用不同的同步原语对其进行基准测试,并且就其而言,使用锁似乎比使用自旋锁具有更好的性能。无论我实际持有锁的时间有多少,使用自旋锁时我收到的争用量远大于使用锁时获得的数量(当然,我在多处理器机器上运行我的测试)。

我意识到在“低级”代码中更有可能遇到一个自旋锁,但是我很想知道你是否觉得它在更高级别的编程中是否有用?

10 个答案:

答案 0 :(得分:32)

这取决于你在做什么。在一般的应用程序代码中,您需要避免使用自旋锁。

在低级别的东西中,你只需要锁定几条指令,并且延迟很重要,螺旋锁是一种比锁更好的解决方案。但这些情况很少见,特别是在通常使用C#的应用程序中。

答案 1 :(得分:20)

在C#中,根据我的经验,“旋转锁定”几乎总是比锁定更糟糕 - 旋转锁定优于锁定的情况很少发生。

然而,并非总是如此。 .NET 4正在添加System.Threading.SpinLock结构。这在锁定被保持很短时间并被反复抓取的情况下提供了益处。来自Data Structures for Parallel Programming上的MSDN文档:

  

在预期等待锁定的情况下,SpinLock提供比其他锁定形式更好的性能。

如果您正在执行诸如锁定树之类的操作,旋转锁可以胜过其他锁定机制 - 如果您在非常短的时间内只对每个节点进行锁定,则它们可以执行传统操作锁。我在一个带有多线程场景更新的渲染引擎中遇到了这个问题 - 在某一点上 - 使用Monitor.Enter将自旋锁分析为优于锁定。

答案 2 :(得分:11)

对于我的实时工作,特别是设备驱动程序,我已经使用了它们。事实证明(当我最后一次定时)等待与硬件中断相关的信号量这样的同步对象咀嚼至少20微秒,无论中断实际发生多长时间。对存储器映射的硬件寄存器进行单次检查,然后检查RDTSC(允许超时以便不锁定机器)处于高纳秒范围内(基本上在噪声中)。对于不应该花费太多时间的硬件级握手,要击败自旋锁是非常困难的。

答案 3 :(得分:9)

我的2c:如果你的更新符合某些访问标准,那么它们就是好的螺旋锁候选者:

  • 快速,即您将有时间获取自旋锁,执行更新并在单个线程量子中释放自旋锁,以便在按住自旋锁时不会被抢占
  • 本地化您更新的所有数据最好都是已加载的一个页面,您不希望在持有自旋锁时出现TLB错误,并且您定义不希望页面错误交换读<!/ LI>
  • atomic 您不需要任何其他锁来执行操作,即。永远不要等待自旋锁下的锁。

对于任何有可能屈服的东西,你应该使用一个通知的锁结构(事件,互斥,信号量等)。

答案 4 :(得分:7)

旋转锁的一个用例是,如果你期望非常低的争用,但会有很多。如果您不需要支持递归锁定,则可以在单个字节中实现自旋锁,如果争用非常低,则CPU周期浪费可以忽略不计。

对于实际用例,我经常有数千个元素的数组,其中对数组的不同元素的更新可以安全地并行发生。两个线程试图同时更新同一个元素的几率非常小(低争用)但我需要为每个元素锁定一个(我将会有很多)。在这些情况下,我通常会分配一个与我正在并行更新的数组大小相同的ubytes数组,并实现内联的自旋锁(在D编程语言中):

while(!atomicCasUbyte(spinLocks[i], 0, 1)) {}
    myArray[i] = newVal;
atomicSetUbyte(spinLocks[i], 0);

另一方面,如果我必须使用常规锁,我将不得不分配一个指向Object的指针数组,然后为该数组的每个元素分配一个Mutex对象。在如上所述的场景中,这只是一种浪费。

答案 5 :(得分:5)

如果您有性能关键代码,您已确定它需要比当前更快,您已确定关键因素是锁定速度,那么试一下自旋锁是个不错的主意。在其他情况下,为什么要这么麻烦?普通锁更容易正确使用。

答案 6 :(得分:4)

请注意以下几点:

  1. 大多数互斥锁的实现在线程实际未调度之前会旋转一段时间。因此,很难将这些互斥锁与纯自旋锁进行比较。

  2. 在同一个螺旋锁上“尽可能快地”旋转的几个线程会占用所有带宽并大大降低程序效率。你需要通过在你的旋转循环中添加noop来增加微小的“休眠”时间。

答案 7 :(得分:3)

你几乎不需要在应用程序代码中使用自旋锁,如果你应该避免使用它们。

我无法在正常操作系统上运行的c#代码中使用自旋锁。繁忙的锁大多是应用程序级别的浪费 - 旋转可以使您使用整个cpu时间片,而锁定将立即导致上下文切换(如果需要)。

高性能代码,你有nr个线程= nr个处理器/核心在某些情况下可能会受益,但是如果你需要在那个级别进行性能优化你可能正在制作下一代3D游戏,那么在同步原语很差的嵌入式操作系统上工作,创建一个OS /驱动程序,或者在任何情况下都不使用c#。

答案 8 :(得分:3)

我在我的HLVM项目中使用自旋锁来处理垃圾收集器的世界各个阶段,因为它们很简单,而且它是一个玩具虚拟机。但是,在这种情况下,自旋锁会适得其反:

格拉斯哥Haskell编译器的垃圾收集器中的一个漏洞是非常烦人的,它有一个名称,“last core slowdown”。这是他们在GC中不恰当地使用自旋锁的直接结果,并且由于其调度程序而在Linux上被激活,但事实上,只要其他程序竞争CPU时间,就可以观察到这种效果。

第二个图here上的效果很明显,可以看到影响的不仅仅是最后一个核心here,其中Haskell程序的性能下降超过了5个核心。

答案 9 :(得分:0)

使用自旋锁时,请始终牢记以下几点:

  • 快速用户模式执行。
  • 在单个进程或共享内存中的多个进程中同步线程。
  • 在拥有该对象之前不返回。
  • 不支持递归。
  • “等待”时消耗100%的CPU。

我个人看到如此多的死锁,只是因为有人认为使用自旋锁是一个好主意。

使用自旋锁时要非常小心

(我对此不太强调)。