ChatGPT解决这个技术问题 Extra ChatGPT

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

我认为两者都在做同样的工作,你如何决定使用哪一个进行同步?

Spinlock Versus Semaphore! 的可能重复项
Mutex 和 Semaphore 不是一回事,所以我不认为这是重复的。引用文章的答案正确地说明了这一点。有关详细信息,请参阅 barrgroup.com/Embedded-Systems/How-To/RTOS-Mutex-Semaphore

M
Mecki

理论

理论上,当一个线程试图锁定一个互斥体并且它没有成功时,因为互斥体已经被锁定,它会进入睡眠状态,立即允许另一个线程运行。它将继续休眠直到被唤醒,一旦互斥锁被之前持有锁的任何线程解锁,就会出现这种情况。当一个线程试图加锁一个自旋锁但没有成功时,它会不断地重试加锁,直到最终成功;因此它不会允许另一个线程取代它的位置(当然,一旦超过当前线程的 CPU 运行时间量子,操作系统将强制切换到另一个线程)。

问题

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

解决方案

在单核/单 CPU 系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞唯一可用的 CPU 内核,就没有其他线程可以运行,并且由于没有其他线程可以运行,锁就不会也可以解锁。 IOW,自旋锁只会在这些系统上浪费 CPU 时间而没有真正的好处。如果线程改为休眠,另一个线程可能会立即运行,可能会解锁锁,然后允许第一个线程继续处理,一旦它再次醒来。

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

实践

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

混合互斥锁起初在多核系统上的行为类似于自旋锁。如果一个线程不能锁定互斥锁,它不会立即进入睡眠状态,因为互斥锁可能很快就会被解锁,所以互斥锁首先会像自旋锁一样工作。只有在经过一定时间(或重试或任何其他测量因素)后仍未获得锁时,线程才真正进入睡眠状态。如果相同的代码在只有一个核心的系统上运行,互斥锁将不会自旋锁,但是,如上所示,这不会有好处。

混合自旋锁起初的行为类似于普通自旋锁,但为了避免浪费过多的 CPU 时间,它可能具有退避策略。它通常不会使线程进入睡眠状态(因为您不希望在使用自旋锁时发生这种情况),但它可能会决定停止线程(立即或在一定时间后;这称为“屈服” ) 并允许另一个线程运行,从而增加了自旋锁被解锁的机会(您仍然有线程切换的成本,但没有让线程进入睡眠状态并再次唤醒它的成本)。

概括

如果有疑问,请使用互斥锁,它们通常是更好的选择,并且大多数现代系统将允许它们在很短的时间内自旋锁定,如果这看起来有益的话。使用自旋锁有时可以提高性能,但只有在某些条件下,并且您有疑问的事实告诉我,您目前没有从事自旋锁可能有益的任何项目。您可能会考虑使用自己的“锁定对象”,它可以在内部使用自旋锁或互斥锁(例如,在创建此类对象时可以配置此行为),最初在任何地方都使用互斥锁,如果您认为在某处使用自旋锁可能真的帮助,试一试并比较结果(例如使用分析器),但一定要测试这两种情况,单核和多核系统,然后再得出结论(如果你的代码可能是不同的操作系统将是跨平台的)。

更新:iOS 警告

实际上不是 iOS 特定的,但 iOS 是大多数开发人员可能面临这个问题的平台:如果你的系统有一个线程调度程序,那并不能保证任何线程,无论它的优先级有多低,最终都会有机会运行,那么自旋锁会导致永久死锁。 iOS调度器区分不同类的线程,低类的线程只有在高类中没有线程想要运行的情况下才会运行。对此没有退避策略,因此如果您永久拥有可用的高级线程,则低级线程将永远不会获得任何 CPU 时间,因此永远不会有机会执行任何工作。

问题出现如下:您的代码在低优先级线程中获得了一个自旋锁,并且当它处于该锁的中间时,时间量已超过并且线程停止运行。再次释放此自旋锁的唯一方法是,如果该低优先级线程再次获得 CPU 时间,但这并不能保证会发生。您可能有几个高优先级的线程,它们不断地想要运行,并且任务调度程序总是会优先考虑这些线程。其中一个可能会跑过自旋锁并尝试获取它,这当然是不可能的,系统会使其屈服。问题是:产生的线程可以立即再次运行!拥有比持有锁的线程更高的优先级,持有锁的线程没有机会获得 CPU 运行时间。其他一些线程将获得运行时或刚刚产生的线程。

为什么互斥锁不会出现这个问题?当高优先级线程无法获得互斥体时,它不会屈服,它可能会旋转一点,但最终会被送入休眠状态。一个休眠的线程在被一个事件唤醒之前是不能运行的,例如,一个它一直在等待的互斥锁被解锁的事件。 Apple 已意识到该问题并因此弃用了 OSSpinLock。新锁称为 os_unfair_lock。这个锁避免了上面提到的情况,因为它知道不同的线程优先级。如果您确定在您的 iOS 项目中使用自旋锁是一个好主意,请使用那个。远离OSSpinLock!在任何情况下都不要在 iOS 中实现您自己的自旋锁!如果有疑问,请使用互斥锁。 macOS 不受此问题的影响,因为它具有不同的线程调度程序,不允许任何线程(即使是低优先级线程)在 CPU 时间上“干涸”,仍然可能出现相同的情况,然后导致非常糟糕性能,因此 OSSpinLock 在 macOS 上也已弃用。


极好的解释...我对自旋锁有疑问,我可以在 ISR 中使用自旋锁吗?如果没有,为什么不
@Mecki如果我没记错的话,我相信您在回答中建议时间切片仅发生在单处理器系统上。这是不正确的!您可以在单处理器系统上使用自旋锁,它会自旋直到其时间段到期。然后另一个具有相同优先级的线程可以接管(就像您为多处理器系统描述的那样)。
@fumoboy007“它会一直旋转直到它的时间段到期”//这意味着你浪费 CPU 时间/电池电量,完全没有任何好处,这完全是愚蠢的。不,我没有说过时间切片只发生在单核系统上,我说在单核系统上只有时间切片,而多核系统有真正的并行性(还有时间切片,但与我在我的文章中写的无关)回复);您也完全错过了混合自旋锁是什么以及为什么它在单核和多核系统上运行良好的观点。
@fumoboy007 线程 A 持有锁并被中断。线程 B 运行并想要锁,但无法获得它,所以它旋转。在多核系统上,线程 A 可以在线程 B 仍在旋转时继续在另一个内核上运行,释放锁,线程 B 可以在其当前时间段内继续运行。在单核系统上,只有一个核心线程 A 可以运行以释放锁,并且该核心由线程 B 旋转保持忙碌。因此,在线程 B 超过其时间量之前,不可能释放自旋锁,因此所有旋转都只是浪费时间。
如果您想了解更多关于在 Linux 内核中实现的 spinlocksmutexes,我强烈建议您阅读 chapter 5 of great Linux Device Drivers, Third Edition (LDD3)(mutexes: page 109; spinlocks: page 116)。
A
AlanC

Mecki的回答很好地说明了这一点。但是,在单个处理器上,当任务正在等待中断服务例程提供的锁时,使用自旋锁可能是有意义的。中断会将控制权转移到 ISR,ISR 将准备好资源以供等待任务使用。它将通过在将控制权交还给中断的任务之前释放锁而结束。旋转任务将找到可用的自旋锁并继续。


我不确定完全同意这个答案。一个单一的处理器,如果一个任务持有一个资源的锁,那么 ISR 就不能安全地继续并且不能等待任务解锁资源(因为持有资源的任务被中断了)。在这种情况下,任务应该简单地禁用中断以强制在它自己和 ISR 之间进行排除。当然,这必须在非常短的时间间隔内完成。
r
reevesy

继续 Mecki 的建议,Alexander Sandler 博客上的这篇文章 pthread mutex vs pthread spinlock,Linux 上的 Alex 展示了 spinlock 和可以实现 mutexes 以使用 #ifdef 测试行为。

但是,请务必根据您的观察进行最后的调用,因为给出的示例是一个孤立的案例,您的项目要求,环境可能完全不同。


C
Community

另请注意,在某些环境和条件下(例如在调度级别 >= 调度级别的窗口上运行),您不能使用互斥锁,而是使用自旋锁。在 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


M
Marcus Thornton

Spinlock 和 Mutex 同步机制在今天非常常见。

让我们首先考虑自旋锁。

基本上它是一个忙碌的等待动作,这意味着我们必须等待指定的锁被释放才能继续下一个动作。概念上很简单,实现的时候就不是这样了。例如:如果锁没有被释放,那么线程被换出并进入睡眠状态,我们应该处理它吗?当两个线程同时请求访问时如何处理同步锁?

通常,最直观的想法是通过变量处理同步以保护临界区。互斥锁的概念类似,但它们仍然不同。关注:CPU 利用率。 Spinlock 等待执行动作会消耗 CPU 时间,因此,我们可以总结一下两者的区别:

在同构多核环境中,如果花在临界区的时间比使用 Spinlock 小,因为我们可以减少上下文切换时间。 (单核对比不重要,因为有些系统在开关中间实现了Spinlock)

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


Y
Yttrill

使用自旋锁的规则很简单:当且仅当持有锁的实时时间是有限的并且足够小时才使用自旋锁。

请注意,通常用户实现的自旋锁不满足此要求,因为它们不会禁用中断。除非禁用抢占,否则在持有自旋锁时抢占会违反有界时间要求。

足够小是一个判断调用,取决于上下文。

例外:即使时间不受限制,某些内核编程也必须使用自旋锁。特别是如果 CPU 没有工作要做,它别无选择,只能旋转直到有更多工作出现。

特别危险:在低级编程中,当存在多个中断优先级时要格外小心(通常至少有一个不可屏蔽中断)。在这种情况下,即使线程优先级的中断被禁用(例如优先级硬件服务,通常与虚拟内存管理相关),也可以运行更高优先级的抢占。如果保持严格的优先级分离,则必须放宽有界实时的条件,并用该优先级的有界系统时间代替。请注意,在这种情况下,不仅可以抢占锁持有人,而且还可以中断微调器;这通常不是问题,因为您无能为力。


K
Ken Birman

自旋锁实际上在 NUMA 机器上的性能很差。这个问题很容易理解并且很难解决(也就是说,无需切换到互斥锁)。考虑一个存在于“靠近”核心 A 的 DRAM 中的自旋锁,并且 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.2us 内获得这个锁。因此,效果可能非常极端。

结论?甚至不要考虑在 NUMA 系统上使用自旋锁,您的线程可能 (1) 在不同的 NUMA 内核上,(2) 与分配自旋锁的 DRAM 具有不同的位置。您将看到大量的性能问题! (3) 使用多线程的第三方库时,请记住,他们可能没有阅读此讨论,可能有错误!


R
Racil Hilan

在单核/单 CPU 系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞唯一可用的 CPU 内核,就没有其他线程可以运行,并且由于没有其他线程可以运行,锁就不会也可以解锁。 IOW,自旋锁只会在这些系统上浪费 CPU 时间而没有真正的好处

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


引用的内容仍然完全正确。如果源代码的编译结果不包含自旋锁,则引用是无关紧要的。假设您所说的关于内核在编译时替换自旋锁的说法是正确的,那么当在另一台可能是或可能不是单处理器的机器上预编译时如何处理自旋锁,除非我们严格地讨论内核本身中的自旋锁?
“一旦进程获得自旋锁,就会禁用抢占”。进程旋转时不会禁用抢占。如果是这样的话,一个进程可以通过进入一个自旋锁并且永不离开来关闭整个机器。请注意,如果您的线程在内核空间(而不是用户空间)中运行,则使用自旋锁确实会禁用抢占,但我认为这不是这里讨论的内容。
在内核编译时?
@konstantin FYI 自旋锁只能在内核空间中使用。并且当使用自旋锁时,本地处理器上会禁用抢占。
@hydranix没明白吗?显然,您不能针对启用了 CONFIG_SMP 的内核编译模块,并在禁用了 CONFIG_SMP 的内核上运行相同的模块。