自旋锁 vs std::mutex::try_lock

8

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

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

2
为什么不只是使用 m.lock();?忙等待有什么意义吗? - Igor Tandetnik
1
在这种情况下,我有许多空闲核心,并希望尽量减少每个线程开始工作的延迟。感觉像是需要自旋锁,但我不明白为什么不常用互斥锁来实现,而是看到了各种自制自旋锁的实现。 - user2411693
2
我怀疑 try_lock() 的成本与 lock() 相当,而您正在承受最坏的情况 - 既需要花费访问内核的代价,又会使用 CPU 作为加热器。 - Igor Tandetnik
那么手写自旋锁的好处在于它保证在用户空间内自旋?如果try_lock需要一个系统调用,那么我将失去上下文并支付延迟,是吗? - user2411693
那是我的感觉,没错。并不是我进行了任何性能测量或其他什么。 - Igor Tandetnik
3
如果您假设您的平台自旋锁实现非常糟糕,以至于即使最简单的实现方式也更好,那么您应该对平台提供的其他东西也做出同样的假设,并放弃使用这个平台。 - David Schwartz
2个回答

12

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

  1. 您的“伪自旋锁”可能会使CPU总线饱和,导致CPU自旋,使其他物理核心(包括持有锁的物理核心)挨饿。

  2. 如果CPU支持超线程或类似功能,您的“伪自旋锁”可能会消耗过多的执行资源,使共享该物理核心的另一个线程挨饿。

  3. 您的“伪自旋锁”可能会进行不必要的写操作,导致缓存行为变糟。当您在x86 / x86_64 CPU上执行读修改写操作时(比如尝试锁定的compare / exchange操作可能会执行的操作),即使值没有改变,也会执行写入。这个写操作会使其他核心上的缓存行失效,需要在另一个核心访问该行时重新共享该行。如果其他核心上的线程同时竞争同一把锁,则情况非常糟糕。

  4. 您的“伪自旋锁”与分支预测交互不良。当您最终获得锁时,在锁定其他线程并需要尽快执行的点上,您会进行所有错误的预测分支。这就像一名跑步者在起跑线准备好奔跑,但当他听到起始枪声后,他停下来喘口气。

基本上,该代码做的一切都是自旋锁可能做错的事情。没有任何东西被有效地处理。编写良好的同步原语需要深入的硬件专业知识。


2
当您在x86 / x86_64 CPU上执行读取修改写入操作(例如compare / exchange,try_lock可能会执行此操作),即使值未更改,它也始终会进行写入。此写入会导致其他核心的缓存行无效,需要在另一个核心访问该行时重新共享它。(请注意,并非所有这些内容都适用于每个平台。) - David Schwartz
1
十年的经验。说真的,我不太确定除了走弯路之外还有什么其他学习方法。 - David Schwartz
1
我认为你提到的很多观点(可能全部)同样适用于链接的“真正”自旋锁。 - MikeMB
@MikeMB 希望不会。如果他们这样做了,你应该提交错误报告!同步原语应该由平台专家实现,他们不会犯这种错误。他们应该深刻理解他们让CPU执行的操作以及CPU制造商推荐的避免策略。(如果您遵循英特尔和AMD的建议,则在x86 CPU上可以避免所有这些问题。) - David Schwartz
1
@DavidSchwartz:你看了链接吗?它并不是指向一个库,而是指向一个博客文章,展示了一个自旋锁的最小示例,几乎与我在这里给出的答案完全相同:https://dev59.com/GF8d5IYBdhLWcg3wpzlf#29195378(这篇博客似乎比我的回答更早,但我当时肯定不知道)。根据try_lock的实现方式,它仍然应该比OP在这里发布的更有效,但它没有任何防止错误分支预测或ht线程饥饿的措施。 - MikeMB
显示剩余3条评论

2
使用自旋锁的主要好处是,如果所有重要的前提条件都成立,那么它的获取和释放非常便宜:锁上没有或很少拥塞
如果您确信不会发生争用,自旋锁将大大优于朴素实现的互斥锁,后者将通过执行您不一定需要的验证来进行库代码,并进行系统调用。这意味着进行上下文切换(消耗数百个周期)并且放弃线程的时间片,导致您的线程被重新调度。在不利条件下,这可能需要无限期等待——即使锁几乎立即可用,您仍然可能需要等待数十毫秒才能再次运行线程。
但是,如果没有争用的前提条件不成立,则自旋锁通常会远远劣于互斥锁,因为它不会取得进展,但它仍会像执行工作一样消耗CPU资源。当阻塞互斥锁时,您的线程不会消耗CPU资源,因此可以将其用于另一个线程来完成工作,或者CPU可以降低速度以节省电源。自旋锁不可能做到这一点,因为它在成功(或失败)之前正在执行“主动工作”。
在最坏的情况下,如果等待者的数量大于CPU核心数,则自旋锁可能会导致巨大的、不成比例的性能影响,因为正在运行的线程正在等待一个永远不会发生的条件(因为释放锁需要另一个线程运行!)。
另一方面,使用自旋锁而不是std::mutex的另一个非技术原因可能是许可条款。许可条款是设计决策的不充分理由,但它们可能仍然非常真实。
例如,当前的GCC实现完全基于pthread,这意味着使用标准线程库的“任何MinGW”都必须链接到winpthreads(没有替代方案)。这意味着您受制于winpthreads许可证,该许可证要求您必须重现其版权消息。对于某些人来说,这是一个无法接受的问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接