你在代码中实际使用自旋锁的频率有多高? 在使用繁忙循环的情况下优于锁的情况有多常见?
个人而言,在编写需要线程安全的某种代码时,我倾向于使用不同的同步原语进行基准测试,就目前而言,使用锁比使用自旋锁性能更好。无论我实际持有锁的时间多短,使用自旋锁时我获得的争用量要比使用锁时获得的争用量大得多(当然,我在多处理器机器上运行我的测试)。
我认识到在“低级”代码中更有可能遇到自旋锁,但我想知道即使在更高级别的编程中,你是否发现它有用?
你在代码中实际使用自旋锁的频率有多高? 在使用繁忙循环的情况下优于锁的情况有多常见?
个人而言,在编写需要线程安全的某种代码时,我倾向于使用不同的同步原语进行基准测试,就目前而言,使用锁比使用自旋锁性能更好。无论我实际持有锁的时间多短,使用自旋锁时我获得的争用量要比使用锁时获得的争用量大得多(当然,我在多处理器机器上运行我的测试)。
我认识到在“低级”代码中更有可能遇到自旋锁,但我想知道即使在更高级别的编程中,你是否发现它有用?
这取决于你要做什么。一般在应用程序代码中,你会想避免自旋锁。
在低级别的东西中,你只需要保持锁定几个指令,并且延迟很重要时,自旋锁可能比锁更好。但这种情况很少见,特别是在通常使用C#的应用程序中。
如果您正在通过树进行锁定,自旋锁可以在某些情况下优于其他锁定机制 - 如果您只需要对每个节点进行非常短的锁定,它们可以胜过传统锁。我曾经在渲染引擎中遇到过这种情况,在多线程场景更新中,自旋锁的性能大幅优于使用Monitor.Enter进行锁定。在等待锁的时间较短的场景下,自旋锁的性能优于其他形式的锁。
在进行实时工作时,尤其是与设备驱动程序相关的工作中,我经常使用它们。事实证明(上次我计时时),等待与硬件中断绑定的同步对象(如信号量)至少需要消耗20微秒的时间,无论实际中断发生的时间多长。单个对内存映射硬件寄存器的检查,紧随其后的是对RDTSC的检查(以允许超时,以避免锁住机器),只需要花费高纳秒级别的时间(基本上在噪音范围内)。对于不应该花费太多时间的硬件级握手来说,自旋锁真的很难被击败。
我的意见是:如果您的更新满足一些访问条件,那么它们就是很好的自旋锁候选者:
对于任何可能产生效果的事情,都应该使用通知的锁结构(事件、互斥体、信号量等)。
自旋锁的一个使用场景是,如果你预计争用非常少,但是需要很多自旋锁。如果您不需要支持递归锁定,则可以在单个字节中实现自旋锁,并且如果争用非常低,则CPU周期浪费可以忽略不计。
对于实际应用场景,我经常有数千个元素的数组,其中可以安全地并行更新数组的不同元素。两个线程同时尝试更新相同元素的概率非常小(低争用),但我需要为每个元素都有一个锁(我将拥有很多这样的元素)。在这些情况下,我通常会分配一个与我并行更新的数组大小相同的无符号字节数组,并将自旋锁内联实现为以下方式(使用D编程语言):
while(!atomicCasUbyte(spinLocks[i], 0, 1)) {}
myArray[i] = newVal;
atomicSetUbyte(spinLocks[i], 0);
另一方面,如果我必须使用常规锁,我将不得不分配一个指向对象的指针数组,并为该数组的每个元素分配一个互斥对象。在上述场景中,这是很浪费的。
大多数互斥锁的实现在线程实际被取消调度之前会自旋一段时间。因此,很难将这些互斥锁与纯自旋锁进行比较。
多个线程在同一个自旋锁上“尽可能快地自旋”会消耗所有带宽,并且极大地降低程序效率。您需要通过在自旋循环中添加noop来添加微小的“休眠”时间。
在应用程序代码中,您几乎从不需要使用自旋锁,如果有什么需要,您应该避免使用它们。
我想不出在正常操作系统上运行的C#代码中使用自旋锁的任何理由。忙锁在应用程序级别上大多是浪费 - 自旋可能导致您使用整个CPU时间片,而锁定将立即在需要时引起上下文切换。
高性能代码,其中线程数=处理器/核心数,在某些情况下可能会受益,但如果您需要在那个级别进行性能优化,则可能正在制作下一代3D游戏,在使用具有较差同步原语的嵌入式操作系统,创建操作系统/驱动程序或以任何情况都不使用C#。
在使用自旋锁时,请始终记住以下几点:
我亲眼见过很多死锁,只是因为有人认为使用自旋锁是一个好主意。
使用自旋锁时一定要非常小心
(我无法强调这一点的重要性)。