旋转锁 vs 标准::互斥::try_lock.

Spinlock vs std::mutex::try_lock

本文关键字:try lock 互斥 旋转 vs 标准      更新时间:2023-10-16

使用专门设计的自旋锁(例如 http://anki3d.org/spinlock(与这样的代码有什么好处:

std::mutex m;
while (!m.try_lock()) {}
# do work
m.unlock();

在典型的硬件上,有巨大的好处:

  1. 您幼稚的"假旋转锁"可能会在 CPU 旋转时使内部 CPU 总线饱和,从而使其他物理内核(包括保持锁的物理内核(匮乏。

  2. 如果 CPU 支持超线程或类似的东西,那么您天真的"假自旋锁"可能会在物理内核上消耗过多的执行资源,从而使共享该物理内核的另一个线程匮乏。

  3. 您幼稚的"假旋转锁"可能会执行无关的写入操作,从而导致不良的缓存行为。当您在 x86/x86_64 CPU 上执行读取-修改-写入操作(类似于try_lock可能执行的比较/交换(时,即使值未更改,它也始终写入。此写入会导致其他内核上的缓存行失效,要求它们在另一个内核访问该行时重新共享该行。如果其他内核上的线程同时争用同一个锁,这很糟糕。

  4. 你天真的"假自旋锁"与分支预测相互作用很差。当你最终得到锁时,你会在锁定其他线程并需要尽快执行的点上采取所有错误预测分支的母。这就像一个跑步者全身鼓起,准备在起跑线上奔跑,但当他听到发令枪时,他停下来喘口气。

基本上,该代码做错了所有事情,旋转锁可能会做错。绝对没有任何东西是有效的。编写良好的同步基元需要深厚的硬件专业知识。

使用旋转锁的主要好处是,如果最重要的先决条件为真,则获取和释放它的成本非常高:锁上很少或没有拥塞

如果您有足够的确定性知道不会有争用,那么自旋锁将大大优于互斥锁的朴素实现,互斥锁将通过库代码进行您不一定需要的验证,并执行系统调用。这意味着执行上下文切换(消耗数百个周期(,并放弃线程的时间片并导致线程重新调度。这可能需要无限期的时间 - 即使锁几乎立即可用,您仍然需要等待几十毫秒才能在不利条件下再次运行线程。

但是,如果无争用的前提条件不成立,则自旋锁通常会大大劣势,因为它没有进展,但它仍然消耗 CPU 资源,就好像它在执行工作一样。在互斥锁上阻塞时,您的线程不会消耗 CPU 资源,因此这些资源可用于其他线程以执行工作,或者 CPU 可能会降低,从而节省电源。这在旋转锁中是不可能的,它正在执行"活动工作",直到成功(或失败(。
在最坏的情况下,如果等待者的数量大于 CPU 内核的数量,旋转锁可能会导致巨大的、不成比例的性能影响,因为处于活动状态并正在运行的线程正在等待在运行时永远不会发生的条件(因为释放锁需要不同的线程才能运行!

另一方面,人们应该期望每个现代的无吸吮std::mutex实现在回退到执行系统调用之前已经包含一个微小的自旋锁。但。。。虽然这是一个合理的假设,但这并不能保证。

使用自旋锁支持std::mutex的另一个非技术原因可能是许可条款。许可条款是设计决策的一个糟糕的理由,但它们可能非常真实。
例如,目前的 GCC 实现完全基于 pthreads,这意味着使用标准线程库中的任何内容的"任何 MinGW"必然与 winpthreads 链接(缺乏替代方案(。这意味着您受 winpthreads 许可证的约束,这意味着您必须复制他们的版权信息。对于某些人来说,这是一个交易破坏者。