旋锁,它们有用吗?

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

我意识到它更可能遇到“低级”代码中的自旋锁,但我很想知道你是否发现它甚至可以用于更高级的编程?

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

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

在C#中,根据我的经验,“自旋锁”几乎总是比锁定更糟糕 – 这种情况很少发生,其中自旋锁的性能优于锁。

然而,情况并非总是如此。 .NET 4正在添加System.Threading.SpinLock结构。 这在锁被保持很短时间并被反复抓取的情况下提供了益处。 从用于并行编程的数据结构的MSDN文档:

在预计等待锁定较短的情况下,SpinLock提供比其他forms的锁定更好的性能。

在您执行诸如锁定树之类的操作的情况下,旋转锁可以胜过其他锁定机制 – 如果您在非常短的时间内仅在每个节点上锁定,则它们可以执行传统锁定。 我在一个带有multithreading场景更新的渲染引擎中遇到了这个问题 – 在某一点上 – 旋转锁被分析出来以超越与Monitor.Enter的锁定。

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

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

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

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

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

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

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

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

如果你有性能关键代码并且你已经确定它需要比现在更快并且你已经确定关键因素是锁定速度,那么尝试自旋锁是个好主意。 在其他情况下,为什么要这么麻烦? 正常锁更容易正确使用。

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

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

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

请注意以下几点:

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

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

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

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

这里的效果很明显,可以看出这里的影响不仅仅是这里的最后一个核心,Haskell程序的性能下降超过了5个核心。