何时应该使用自旋锁而不是互斥锁?

378

我认为两者都完成了相同的工作,您如何决定使用哪个进行同步?


1
可能重复了自旋锁和信号量的比较! - Paul R
17
Mutex和Semaphore并不是同一件事情,因此我不认为这是一个重复的问题。参考文章的答案已经正确地表述了这一点。更多详情请查看:http://www.barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore - nanoquack
9个回答

917

理论

在理论上,当一个线程尝试锁定互斥锁但失败时(因为该锁已经被锁定),它会进入睡眠状态,立即允许另一个线程运行。它将继续睡眠,直到被唤醒,这将在之前持有锁的任何线程解锁该互斥锁时发生。当一个线程尝试锁定自旋锁但失败时,它将不断尝试锁定它,直到最终成功为止;因此它不会允许另一个线程替代它(然而,当当前线程的 CPU 运行时间量已超过时,操作系统将强制切换到另一个线程)。

问题

互斥锁的问题在于,将线程置于睡眠状态并将它们唤醒的操作都是非常昂贵的操作,它们需要相当多的 CPU 指令,因此也需要一些时间。如果现在互斥锁只被锁定了很短的时间,那么将一个线程置于睡眠状态并唤醒它所花费的时间可能会远远超过线程实际睡眠的时间,并且甚至可能超过线程通过不断轮询自旋锁浪费的时间。另一方面,在自旋锁上轮询将不断浪费 CPU 时间,如果锁被持有了较长的时间,这将浪费更多的 CPU 时间,如果线程睡眠会更好。

解决方案

在单核/单 CPU 系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞了唯一可用的 CPU 核心,就没有其他线程可以运行,因此也没有其他线程可以解锁该锁。换句话说,在这些系统上,自旋锁只是浪费 CPU 时间而没有实际好处。如果将线程置于睡眠状态,另一个线程可以立即运行,可能解锁该锁,然后在该线程再次唤醒后允许第一个线程继续处理。

在多核/多 CPU 系统上,当有许多锁仅被持有了很短的时间时,不断将线程置于睡眠状态并唤醒它们所浪费的时间可能会明显降低运行时性能。当使用自旋锁时,线程有机会利用其完整的运行时间量(始终只阻塞很短的时间混合互斥锁在多核系统上的行为类似于自旋锁。如果线程无法锁定互斥锁,它不会立即进入睡眠状态,因为互斥锁可能很快被解锁,因此互斥锁会首先表现得像自旋锁一样。只有在一定时间(或重试或任何其他测量因素)后仍未获得锁时,线程才会真正进入睡眠状态。如果相同的代码在只有一个核心的系统上运行,则互斥锁将不会自旋锁定,因为像上面所说,这并不是有利的。

混合自旋锁起初的行为类似于普通自旋锁,但为了避免浪费太多CPU时间,它可以采用回退策略。它通常不会让线程睡眠(因为在使用自旋锁时不希望发生这种情况),但它可能决定停止线程(立即或在一定时间后;这称为“屈服”),并允许另一个线程运行,从而增加自旋锁解锁的机会(您仍然需要线程切换的成本,但不需要将线程置于睡眠状态并再次唤醒它的成本)。
总之,如果有疑问,请使用互斥锁,它们通常是更好的选择,大多数现代系统都允许它们在非常短的时间内自旋锁定,如果这似乎有益。使用自旋锁有时可以提高性能,但仅在某些条件下,并且您对此存在疑虑告诉我,您目前没有在任何可能受益于自旋锁的项目上工作。您可以考虑使用自己的“锁对象”,该对象可以在内部使用自旋锁或互斥锁(例如,在创建这种对象时可以配置此行为),最初在所有地方使用互斥锁,如果您认为在某个地方使用自旋锁可能会真正有所帮助,请尝试并比较结果(例如,使用分析器),但一定要在跳出结论之前测试单核和多核系统中的两种情况(以及可能的不同操作系统,如果您的代码将跨平台)。
更新:iOS的警告
实际上并非仅限于iOS,但iOS是大多数开发人员可能面临该问题的平台:如果您的系统具有不保证任何线程(无论其优先级有多低)最终都会有机会运行的线程调度程序,那么自旋锁可能会导致永久死锁。 iOS调度程序将不同的线程类别区分开来,并且低级别线程只有在没有高级别线程想要运行时才会运行。没有回退策略,因此如果您始终可用高级别线程,则低级别线程永远不会获得任何CPU时间,因此永远不会有机会执行任何工作。问题的表现如下:您的代码在低优先级类线程中获得自旋锁,当它在该锁的中间时,时间量已超过并且线程停止运行。这个自旋锁能够再次释放的唯一方式是如果该低优先级类线程再次获得CPU时间,但不能保证这一点。您可能有几个不断想要运行的高优先级类线程,任务调度器将始终对它们进行优先排序。其中一个线程可能会遇到自旋锁并尝试获取它,当然这是不可能的,并且系统将使其屈服。问题是:一个屈服的线程立即可以再次运行!比持有锁的线程的优先级更高,持有锁的线程没有机会获得CPU运行时间。要么其他某个线程将获得运行时间,要么刚刚屈服的线程将获得时间。
为什么互斥锁不会出现这个问题?当高优先级线程无法获取互斥锁时,它不会屈服,它可能会稍微自旋一下,但最终会被发送到睡眠状态。睡眠的线程在被事件唤醒之前不可用于运行,例如等待的互斥锁已解锁。苹果公司已经意识到这个问题,并因此弃用了OSSpinLock。新的锁称为os_unfair_lock。该锁避免了上述情况,因为它知道不同的线程优先级类别。如果您确定在iOS项目中使用自旋锁是一个好主意,则请使用该锁。远离OSSpinLock!在iOS中绝不能实现自己的自旋锁!如有疑问,请使用互斥锁。macOS没有受到这个问题的影响,因为它有一个不允许任何线程(即使是低优先级线程)在CPU时间上“干涸”的不同线程调度程序,但仍可能出现相同的情况,然后会导致非常差的性能,因此OSSpinLock在macOS上也已被弃用。

5
非常好的解释...我有一个关于自旋锁的疑问,即我能否在中断服务程序中使用自旋锁?如果不能,为什么不能? - haris
6
如果我没理解错的话,我相信你在回答中提到时间片轮转只发生在单处理器系统上。但是这是不正确的!你可以在单处理器系统上使用自旋锁,它会一直自旋直到时间片用完。然后同样优先级的另一个线程就可以接管(就像你描述多处理器系统一样)。 - fumoboy007
8
@fumoboy007说:"并且它将持续旋转,直到其时间量子过期",这意味着你浪费CPU时间/电池电量而毫无益处,这是极其愚蠢的。我并没有说时间片只在单核系统上发生,我说的是在单核系统上只有时间片,而在多核系统上有真正的并行处理(也有时间片,但与我在回复中写的内容无关);此外,你完全没有理解混合自旋锁的重点以及为什么它在单核和多核系统上都能很好地工作。 - Mecki
11
@fumoboy007 线程A持有锁并被中断。线程B运行并想要锁,但无法获得它,因此它会一直自旋。在多核系统中,当线程B仍在自旋时,线程A可以继续在另一个核上运行,释放锁,然后线程B可以在其当前时间片内继续。在单核系统中,只有一个核心可以让线程A运行以释放锁,而该核心正被线程B自旋占用。因此,在线程B超出其时间片之前,自旋锁永远不可能被释放,因此所有自旋都是浪费时间的。 - Mecki
6
如果你想了解在Linux内核中实现的自旋锁和互斥锁,我强烈推荐阅读Linux设备驱动程序第三版 (LDD3)第5章(互斥锁:第109页;自旋锁:第116页)。 - patryk.beza
显示剩余9条评论

8

根据Mecki的建议,这篇文章pthread mutex vs pthread spinlock在Alexander Sandler的博客Alex on Linux上展示了如何使用#ifdef实现spinlockmutexes以测试它们的行为。

然而,请确保基于你的观察和理解做出最终决定,因为所给出的示例是一个孤立的案例,你的项目需求和环境可能完全不同。


8

Mecki的答案非常好。然而,在单处理器上,当任务正在等待中断服务例程给予锁时,使用自旋锁可能是有意义的。中断将控制传输到ISR,ISR将准备资源供等待任务使用。它将在将锁释放之前结束,并将控制权交还给被中断的任务。自旋任务将发现自旋锁可用并继续执行。


2
我不确定是否完全同意这个答案。在单个处理器上,如果任务持有资源的锁,则ISR无法安全地继续执行,并且不能等待任务解锁该资源(因为正在持有资源的任务被中断)。在这种情况下,任务应该简单地禁用中断以强制在其自身和ISR之间进行排除。当然,这必须在非常短的时间间隔内完成。 - user1202136
@user1202136 “等待中断服务例程提供锁定。”他的意思是ISR将解锁资源。 - Motomotes

6
请注意,在某些环境和条件下(例如在Windows上运行时,调度级别> = DISPATCH LEVEL),您不能使用互斥锁,而必须使用自旋锁。在Unix上也是如此。
以下是竞争对手 StackExchange Unix 网站上的等效问题:https://unix.stackexchange.com/questions/5107/why-are-spin-locks-good-choices-in-linux-kernel-design-instead-of-something-more 有关Windows系统分派的信息:http://download.microsoft.com/download/e/b/a/eba1050f-a31d-436b-9281-92cdfeae4b45/IRQL_thread.doc

5
Spinlock和Mutex同步机制在今天非常常见。
首先让我们考虑Spinlock。
基本上,它是一种繁忙等待的操作,这意味着我们必须等待特定的锁被释放,然后才能进行下一步操作。概念上非常简单,但实现起来却不是那么容易。例如:如果锁没有被释放,那么线程会被交换出去并进入睡眠状态,我们应该如何处理它?当两个线程同时请求访问时,如何处理同步锁?
通常,最直观的想法是通过变量来保护关键部分进行同步。Mutex的概念类似,但它们仍然有所不同。重点是:CPU利用率。Spinlock消耗CPU时间来等待执行操作,因此,我们可以总结出两者之间的差异:
在同质多核环境中,如果关键部分花费的时间很短,则使用Spinlock,因为我们可以减少上下文切换时间。(单核比较不重要,因为某些系统实现将Spinlock放在切换中间)
在Windows中,使用Spinlock将升级线程到DISPATCH_LEVEL,在某些情况下可能是不允许的,因此这时我们必须使用Mutex(APC_LEVEL)。

1
使用自旋锁的规则很简单:当且仅当锁定的实际时间是有限且足够短时,才使用自旋锁。
请注意,通常用户实现的自旋锁不满足此要求,因为它们不会禁用中断。除非禁止抢占,否则在持有自旋锁时进行抢占会违反有限时间要求。
足够小是一个判断调用,并取决于上下文。
特例:一些内核编程必须在时间不受限制时使用自旋锁。特别是如果CPU没有工作要做,它别无选择,只能自旋直到出现更多工作。
特别危险:在低级编程中,当存在多个中断优先级(通常至少有一个不可屏蔽中断)时,请格外小心。在这种更高的优先级中,即使线程优先级的中断被禁用(例如与虚拟内存管理相关的优先级硬件服务),也可以运行更高优先级的抢占。只要保持严格的优先级分离,就必须放宽实际时间的条件,并用该优先级水平上的有限系统时间替换。请注意,在这种情况下,不仅可能会抢占锁持有者,还可能会中断自旋者;这通常不是问题,因为你无能为力。

0

自旋锁在NUMA机器上的表现实际上可能非常糟糕。这个问题很容易理解,但很难修复(除非切换到互斥锁)。考虑一个生活在DRAM“附近”的A核心上的自旋锁,以及在A和B上竞争该锁的线程。假设B远离这个DRAM。正如我们所知道的那样,这意味着A的内存访问速度将比B的内存访问速度快5倍左右,因为B的访问需要穿过NUMA芯片的总线,而A的访问是本地的,因此避免了总线遍历。

实际上,A的自旋逻辑将比B的快5倍或更多。是的,它们会竞争,B会打扰A,但影响是不对称的:当A赢得下一次访问锁的竞赛时,它将获得本地加载和存储,因此将以更高的指令速率旋转。当B旋转时,那些远程加载和存储将会很慢,因此B会以慢动作旋转。

结论是,我们在Derecho上的工作中观察到,我们获得了一个非常不公平的自旋锁。 A比B更受青睐,通过B进行锁定将需要很长时间。

你会如何观察这个问题?在我们的情况下,我们使用LibFabrics,该库有几个线程分散在多个核心上。在LibFabric逻辑中,A和B旋转以锁定并检查与RDMA硬件相关联的完成队列。因此,效果是A比B更频繁地检查此队列5倍。在需要B执行操作(该队列头部的已完成操作由B拥有)的情况下,A有效地使B无法访问--极大地减慢了LibFabrics的速度,并对我们的Derecho代码产生了巨大影响。我们曾经看到过这样的情况,即A的访问受到强烈青睐,以至于B可能等待长达10毫秒的锁--即使在非争用情况下,B也可以在0.2微秒内获取此锁。因此,影响可能非常极端。
结论?在 NUMA 系统上,不要考虑使用自旋锁,因为你的线程可能会 (1) 在不同的 NUMA 核心上,(2) 与分配自旋锁的 DRAM 的本地性不同。你将会遇到巨大的性能问题!(3) 当使用具有多个线程的第三方库时,请记住他们可能没有阅读此讨论并且可能是错误的!

0
Spinlock非常轻量级,通常会禁用中断并在SMP上自旋。互斥锁有许多要求。互斥锁是一种睡眠锁。它包含一个队列,线程在等待互斥锁时可以放置在队列上。当互斥锁被释放时,操作系统可以直接切换到第一个未阻塞的线程。队列通常也是基于优先级的,意味着最高优先级的线程将能够立即运行。自旋锁没有这些功能,主要用于处理CPU之间的并发。

-7
在单核/单CPU系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞了唯一可用的CPU核心,就没有其他线程可以运行,而且由于没有其他线程可以运行,锁也不会被解锁。换句话说,在这些系统上,自旋锁只会浪费CPU时间,没有真正的好处。
这是错误的。在单处理器系统上使用自旋锁不会浪费CPU周期,因为一旦进程获取自旋锁,抢占就被禁用了,因此,就不可能有其他人在自旋!只是使用它没有任何意义!因此,内核在编译时通过preempt_disable替换Uni系统上的自旋锁!

引用仍然完全正确。如果源代码的编译结果不包含自旋锁,则引用无关紧要。假设您所说的内核在编译时替换自旋锁是正确的,那么当在另一台可能是单处理器或多处理器的机器上预编译时,如何处理自旋锁,除非我们严格只谈论内核本身中的自旋锁? - Hydranix
2
一旦进程获取自旋锁,抢占就被禁用了。但是当一个进程获取自旋锁时,并不会禁用抢占。如果这样的话,一个进程只需进入自旋锁并永远不离开,就可以使整个机器崩溃。请注意,如果您的线程在内核空间运行(而不是用户空间),获取自旋锁确实会禁用抢占,但我认为这不是正在讨论的问题。 - Konstantin Weitz
编译时内核完成? - Shien
@konstantin FYI,自旋锁只能在内核空间中获取。当获取自旋锁时,本地处理器上的抢占会被禁用。 - Neelansh Mittal
@hydranix 没听懂你的意思?显然,你不能在启用了 CONFIG_SMP 的内核中编译模块,并在禁用了 CONFIG_SMP 的内核上运行相同的模块。 - Neelansh Mittal

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