什么时候应该使用自旋锁而不是互斥锁?


Answers:


729

理论

从理论上讲,当一个线程试图锁定一个互斥锁而没有成功时,由于该互斥锁已经被锁定,它将进入睡眠状态,并立即允许另一个线程运行。它会继续睡眠直到被唤醒为止,一旦互斥量被之前持有该锁的任何线程解锁,情况就是如此。当线程尝试锁定自旋锁而没有成功时,它将不断重试锁定它,直到最终成功;否则,它将继续尝试锁定自旋锁。因此,它将不允许其他线程代替(但是,一旦超出当前线程的CPU运行时范围,操作系统将强制切换到另一个线程)。

问题

互斥锁的问题在于使线程进入睡眠状态并再次唤醒它们都是相当昂贵的操作,它们将需要大量的CPU指令,因此也需要一些时间。如果现在互斥锁仅锁定了很短的时间,则使线程进入睡眠状态并再次唤醒它所花费的时间可能会超过该线程实际睡眠的时间,甚至可能超过该线程将要休眠的时间。不断轮询自旋锁浪费了时间。另一方面,对自旋锁进行轮询将不断浪费CPU时间,如果将锁保持更长的时间,则将浪费更多的CPU时间,而如果线程正在休眠则更好。

解决方案

在单核/单CPU系统上使用自旋锁通常是没有意义的,因为只要自旋锁轮询阻止了唯一可用的CPU内核,其他线程就无法运行,并且由于没有其他线程可以运行,因此锁不会被解锁。IOW,自旋锁只会浪费那些系统上的CPU时间,而没有真正的好处。如果改为让该线程进入睡眠状态,则另一个线程可能会立即运行,可能会解除锁定,然后在第一个线程再次唤醒后允许其继续处理。

在多核/多CPU系统上,只有大量锁定仅在很短的时间内保持不变,浪费时间不断使线程进入睡眠状态并再次唤醒它们可能会显着降低运行时性能。相反,当使用自旋锁时,线程有机会利用其完整的运行时域(总是仅在很短的时间内阻塞,但随后立即继续其工作),从而导致更高的处理吞吐量。

实践

由于程序员通常无法事先知道互斥或自旋锁是否会更好(例如,因为目标体系结构的CPU内核数量未知),因此操作系统也无法知道某个代码段是否已针对单核或Windows优化。在多核环境中,大多数系统并不严格区分互斥锁和自旋锁。实际上,大多数现代操作系统都具有混合互斥锁和混合自旋锁。这实际上是什么意思?

混合互斥锁首先在多核系统上表现得像自旋锁。如果线程无法锁定互斥锁,则不会立即将其置于睡眠状态,因为互斥锁可能很快就会被解锁,因此互斥锁将首先完全像自旋锁一样工作。只有在一定时间(或重试或任何其他测量因素)之后仍未获得锁定时,线程才真正进入睡眠状态。如果相同的代码在只有一个内核的系统上运行,则互斥锁将不会自旋锁,但是,如上所述,这将无济于事。

混合自旋锁起初的行为类似于普通自旋锁,但为避免浪费过多的CPU时间,它可能有一个后退策略。它通常不会使线程进入睡眠状态(因为使用自旋锁时您不希望这种情况发生),但是它可能会决定(立即或在一定时间后)停止线程并允许另一个线程运行,因此增加了自旋锁被解锁的机会(纯线程切换通常比使线程进入睡眠状态并稍后唤醒它的便宜,尽管目前为止还不算很贵)。

摘要

如果有疑问,请使用互斥锁,它们通常是更好的选择,如果这看起来很有用,大多数现代系统将允许它们在很短的时间内自旋。使用自旋锁有时可以提高性能,但是仅在某些条件下,您有疑问的事实告诉我,您目前不在从事自旋锁可能有益的任何项目。您可以考虑使用自己的“锁定对象”,该对象可以在内部使用自旋锁或互斥锁(例如,在创建此类对象时可以配置此行为),最初在各处使用互斥锁,如果您认为在某个地方使用自旋锁可能确实帮助,尝试一下并比较结果(例如使用探查器),但一定要测试两种情况,

更新:iOS警告

实际上,不是特定于iOS的平台,而是大多数开发人员可能会遇到此问题的平台:如果您的系统具有线程调度程序,则不能保证任何线程(无论其优先级多么低)最终都有机会运行,那么自旋锁会导致永久性的死锁。iOS调度程序会区分线程的不同类别,并且仅当高级类别中的任何线程都不希望运行时,低级类别的线程才会运行。没有任何退避策略,因此,如果您永久拥有高级线程,则低级线程将永远不会获得任何CPU时间,因此也就不会有任何机会执行任何工作。

问题出现如下:您的代码在一个低prio类线程中获得了一个自旋锁,当它位于该锁的中间时,时间段已超过,该线程停止运行。再次释放该自旋锁的唯一方法是,如果该低prio类线程再次获得CPU时间,但不能保证会发生这种情况。您可能有几个经常要运行的高级prio类线程,任务计划程序将始终对它们进行优先级排序。其中之一可能会碰到自旋锁并尝试获取它,这当然是不可能的,并且系统会使其屈服。问题是:产生的线程立即可以再次运行!具有比持有锁的线程更高的优先级,持有锁的线程没有机会获得CPU运行时。

为什么互斥锁不会出现此问题?当高prio线程无法获取互斥体时,它不会屈服,它可能会旋转一点,但最终会进入睡眠状态。睡眠线程只有在被事件唤醒后才可以运行,例如,等待互斥锁被解锁的事件。苹果公司意识到了这个问题,因此已弃用OSSpinLock。新锁称为os_unfair_lock。此锁避免了上述情况,因为它知道不同的线程优先级。如果您确定在您的iOS项目中使用自旋锁是个好主意,请使用该锁。保持距离OSSpinLock!而且在任何情况下都不能在iOS中实现自己的自旋锁!如有疑问,请使用互斥锁!macOS不受此问题的影响,因为它具有不同的线程调度程序,该调度程序不允许任何线程(甚至是低prio线程)在CPU时间上“干运行”,在那里仍然可能出现相同的情况,从而导致性能很差性能,因此OSSpinLock在macOS上也已弃用。


3
极好的解释...我对自旋锁有疑问,我可以在ISR中使用自旋锁吗?如果没有,为什么
哈里斯(Haris)2014年

4
@Mecki如果我没记错的话,我相信您在回答中建议时间分片仅发生在单处理器系统上。这是不对的!您可以在单处理器系统上使用自旋锁,它将一直旋转直到其时间到期为止。然后,具有相同优先级的另一个线程可以接管(就像您在多处理器系统中所描述的那样)。
fumoboy007 2014年

7
@ fumoboy007“它将旋转,直到其时间量子到期为止” //这意味着您浪费CPU时间/电池电量绝对没有任何好处,而没有任何单一的好处,这完全是愚蠢的。不,我无话可说,时间片只发生在单核系统上,我说在单核系统上只有时间片,而多核系统却有真正的并行性(时间片也与我写的内容无关)回复); 您也完全错过了混合自旋锁的含义以及为什么它在单核和多核系统上都能很好地工作。
Mecki 2014年

11
@ fumoboy007线程A持有锁并被中断。线程B运行并想要锁,但无法获取它,因此它旋转。在多核系统上,当线程B仍在旋转时,线程A可以继续在另一个内核上运行,释放锁,线程B可以在其当前时间范围内继续运行。在单核系统上,只有一个核Thread A可以运行以释放锁,并且线程B旋转使该核处于繁忙状态。因此,在线程B超过其时间范围之前,不可能释放自旋锁,因此所有旋转都只是浪费时间。
Mecki 2014年

2
如果您想了解有关在Linux内核中实现的自旋锁互斥锁的更多信息,我强烈建议您阅读出色的Linux设备驱动程序第三版(LDD3)的第5章(互斥锁:第109页;自旋锁:第116页)。
patryk.beza

7

继续Mecki的建议,Alexander Sandler博客上的pthread互斥锁与pthread自旋锁,Linux上的Alex展示了如何使用#ifdef实现spinlockmutexes来测试行为。

但是,请确保根据您的观察进行最终决定,因为给出的示例是一个单独的案例,因此您的项目要求,环境可能完全不同。


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-更多

Windows系统上的分派信息:http : //download.microsoft.com/download/e/b/a/eba1050f-a31d-436b-9281-92cdfeae4b45/IRQL_thread.doc


6

梅基的答案很好。但是,在单个处理器上,当任务正在等待中断服务例程提供的锁时,使用自旋锁可能很有意义。中断会将控制权转移给ISR,ISR将为等待任务使用的资源做好准备。在将控制权交还给被中断的任务之前,它会先释放锁定。旋转任务将找到可用的自旋锁并继续。


2
我不确定是否完全同意这个答案。一个单一处理器,如果一个任务在资源上持有锁,则ISR无法安全进行,也无法等待该任务解锁资源(因为持有资源的任务被中断)。在这种情况下,任务应仅禁用中断以强制其自身与ISR之间进行排除。当然,这必须在非常短的时间间隔内完成。
user1202136

3

自旋锁和互斥锁同步机制今天非常普遍。

让我们首先考虑一下Spinlock。

基本上,这是一个繁忙的等待操作,这意味着我们必须等待指定的锁释放后才能继续执行下一个操作。从概念上讲非常简单,但实现时并非如此。例如:如果尚未释放锁,则线程被换出并进入睡眠状态,我们应该处理它吗?当两个线程同时请求访问时,如何处理同步锁?

通常,最直观的想法是通过变量处理同步以保护关键部分。Mutex的概念相似,但是仍然不同。专注于:CPU利用率。自旋锁会消耗CPU时间来等待执行操作,因此,我们可以总结两者之间的区别:

在同类多核环境中,如果在关键部分花费的时间少于使用Spinlock,则可以减少上下文切换时间。(单核比较并不重要,因为某些系统在开关中间实现了Spinlock)

在Windows中,使用Spinlock会将线程升级到DISPATCH_LEVEL,在某些情况下可能不允许这样做,因此这次我们不得不使用Mutex(APC_LEVEL)。


-6

在单核/单CPU系统上使用自旋锁通常是没有意义的,因为只要自旋锁轮询阻止了唯一可用的CPU内核,其他线程就无法运行,并且由于没有其他线程可以运行,因此锁不会被解锁。IOW,自旋锁仅浪费那些系统上的CPU时间,没有任何真正的好处

错了 在单处理器系统上使用自旋锁不会浪费cpu周期,因为一旦一个进程获得了自旋锁,就将禁用抢占,因此,就不会有其他人在旋转!只是使用它没有任何意义!因此,Uni系统上的自旋锁在编译时被内核替换为preempt_disable!


引用仍然完全正确。如果源代码的编译结果不包含自旋锁,则引用的内容无关紧要。假设您所说的关于内核在编译时替换自旋锁的说法是正确的,那么除非在严格意义上仅在内核本身中谈论自旋锁,否则在另一个可能是也可能不是单处理器的计算机上预编译自旋锁时,如何处理自旋锁?
Hydranix

“一旦进程获得了自旋锁,则将禁用抢占”。进程旋转时,不会禁用抢占。如果是这样的话,只需输入一个自旋锁而永不离开,单个进程就可以摧毁整个机器。请注意,如果您的线程在内核空间(而不是用户空间)中运行,则使用自旋锁确实会禁用抢占,但是我不认为这是在这里讨论的。
康斯坦丁·威茨

编译时通过内核
Shien

@konstantin FYI自旋锁只能在内核空间中使用。并且当采取了自旋锁时,本地处理器上的抢占被禁用。
Neelansh Mittal '18

@hydranix没明白吗?显然,您无法针对已启用CONFIG_SMP的内核编译模块,而无法在已禁用CONFIG_SMP的内核上运行同一模块。
Neelansh Mittal,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.