【问题标题】:Spinlock vs std::mutex::try_lock自旋锁与 std::mutex::try_lock
【发布时间】:2016-05-21 17:59:34
【问题描述】:

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

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

【问题讨论】:

  • 为什么不只是m.lock();?忙碌的等待有什么意义?
  • 在这种情况下,我有许多空闲的内核,并且希望最小化每个线程开始工作的延迟。感觉自旋锁是我需要的,但我不明白为什么使用互斥锁不常见,而且我一直看到不同的自旋锁实现。
  • 我怀疑try_lock()lock() 一样贵,而且你得到了两全其美的结果——访问内核的成本,以及将你的 CPU 用作空间加热器.
  • 那么手动自旋锁的好处是它可以保证在用户空间中旋转?如果 try_lock 需要系统调用,那么我将失去上下文并支付延迟?
  • @user2411693 如果你认为你的平台的自旋锁实现是如此糟糕,以至于即使是最简单的实现也更好,你应该假设你的平台也提供了其他东西,然后放弃平台。

标签: c++ mutex spinlock


【解决方案1】:

在典型的硬件上,有很多好处:

  1. 当 CPU 旋转时,您天真的“假自旋锁”可能会使内部 CPU 总线饱和,从而导致其他物理内核(包括持有锁的物理内核)处于饥饿状态。

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

  3. 您天真的“假自旋锁”可能会执行导致不良缓存行为的无关写入操作。当您在 x86/x86_64 CPU 上执行读取-修改-写入操作时(例如 try_lock 可能执行的比较/交换),即使值未更改,它也会始终写入。此写入导致缓存行在其他内核上无效,要求它们在另一个内核访问该行时重新共享它。如果其他内核上的线程同时争用同一个锁,那就太糟糕了。

  4. 你天真的“假自旋锁”与分支预测的交互很糟糕。当您最终获得锁定时,您会在锁定其他线程并需要尽快执行的位置获取所有错误预测分支的母亲。这就像一个跑者在起跑线上准备好奔跑,但当他听到发令枪响起时,他停下来喘口气。

基本上,该代码做错了自旋锁可能做错的所有事情。绝对没有什么能有效地完成。编写好的同步原语需要深厚的硬件专业知识。

【讨论】:

  • 我遵循 #1、#2 和 #4。但我不明白#3。你能详细说明一下吗。欣赏你如何果断地指出这是一个坏主意。吸取的教训,将用适当的自旋锁取代所有这些用途。
  • 当您在 x86/x86_64 CPU 上执行 read-modify-write 操作(例如 try_lock 可能执行的比较/交换)时,即使值未更改,它也会始终写入。此写入导致缓存行对其他内核无效,要求它们在另一个内核访问该行时重新共享它。 (请注意,并非所有这些都适用于每个平台。)
  • 十年经验。说真的,我真的不知道如何学习这些东西,除了困难的方法。
  • 我认为您的很多观点(可能全部)同样适用于链接的“真实”自旋锁。
  • @DavidSchwartz:你看过链接了吗?它没有链接到库,而是链接到显示自旋锁的最小示例的博文,这与我在这里给出的答案几乎相同:stackoverflow.com/questions/26583433/…(该博客似乎先于我的答案,但我确定我那时候还不知道)。根据 try_lock 的实现方式,它应该仍然比 OP 在此处发布的更有效,但它没有任何针对错误分支预测或 ht-thread 饥饿的规定。
【解决方案2】:

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

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

但是,如果不存在争用的前提条件不成立,则自旋锁通常会大大劣势,因为它没有进展,但它仍然像在执行工作一样消耗 CPU 资源。在互斥体上阻塞时,您的线程不会消耗 CPU 资源,因此可以将这些资源用于不同的线程来完成工作,或者 CPU 可能会减速,从而节省电力。这对于自旋锁来说是不可能的,它一直在做“主动工作”,直到它成功(或失败)。
在最坏的情况下,如果等待者的数量大于 CPU 核心的数量,自旋锁可能会导致巨大,不成比例的性能影响,因为活动和运行的线程正在等待一个永远不可能的条件在它们运行时发生(因为释放锁需要不同的线程来运行!)。

另一方面,人们应该期望std::mutex 的每一个现代 no-suck 实现都已经包含一个微小的自旋锁,然后再回退到执行系统调用。但是……虽然这是一个合理的假设,但不能保证。

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

【讨论】:

    猜你喜欢
    • 2018-06-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-03-28
    • 1970-01-01
    • 1970-01-01
    • 2013-12-29
    相关资源
    最近更新 更多