为什么自旋锁会成为多线程程序中的性能问题?

Why spinlocks can become performance issue in multithreaded programs?

我知道什么是自旋锁并且它们使用忙等待。但是为什么在多核处理器上的多线程程序中会成为性能问题呢?

对此可以做些什么?

您的第一个问题是,在自旋锁保护部分变得满足的情况下,通常是准备执行的线程多于可用内核的情况。这意味着在自旋锁中浪费时间的每个线程都可能使另一个线程处于饥饿状态,而该线程本来可以做一些适当的事情。

然后是 spinock 本身的成本。您正在耗尽内存事务的预算,而该预算实际上是在处理器内核之间共享的。实际上,这会导致关键部分内的操作变慢。

一个很好的例子是 Windows 内核中的内存分配器,在 1703 和 1803 之间的版本中。在具有超过 16 个线程的系统上,一旦 CPU 总利用率达到 50%超过时,该路径中的自旋锁失去控制,将开始占用 90% 的 CPU 时间。由于竞争线程消耗了内存带宽,关键部分内花费的时间增加了十倍以上。


天真的解决方案是在自旋周期之间使用纳米睡眠,以至少降低锁本身的性能消耗。但这也很糟糕,因为核心仍然被阻塞,没有做任何实际工作。

尝试让出自旋锁?只是转得更慢,你最终得到一个与操作系统的调度率成正比的最小延迟。以 1ms(Windows 实时模式,任何进程请求时激活)、5ms(Linux 默认 200Hz 调度程序)、10ms(Windows 默认模式)的速率,这是一个巨大的延迟正在执行中。而且,如果您碰巧再次命中关键部分,那将是一种浪费,因为您现在添加了上下文切换的开销而没有任何收益。


最终,将操作系统原语用于临界区。常见的方法是使用原子操作 探测 是否发生任何争用,当争用发生时,才涉及操作系统。

无论哪种方式,下面的操作系统都有更好的方法来解决争用,主要是等待列表的形式。这意味着等待信号量的线程只会在它们被允许恢复时恰好等待,并保证持有相应的锁。当离开竞争区域时,拥有锁的线程会通过轻量级方式检查是否存在任何竞争,只有在存在竞争时才会通知 OS 恢复对其他线程的操作。


并不是说你真的应该重新发明轮子...

在 Windows 中,这已经是 Slim Reader/Writer Locks 的实现方式。 如果你使用普通的 std::mutex 或类似的东西,你通常会在幕后得到这样的机制。

"Old" 文献(10-15 年)仍然会警告您不要使用 OS 原语进行调度,但这已经严重过时并且没有反映对 OS 边。过去每次上下文切换的延迟都超过 10 毫秒,如今基本上已经无法测量了。