我认为两者都在做同样的工作,您如何决定使用哪一个进行同步呢?
这个理论
理论上,当一个线程试图锁定一个互斥锁但由于互斥锁已经被锁定而没有成功时,它将进入睡眠状态,立即允许另一个线程运行。它将继续休眠,直到被唤醒,一旦互斥锁被之前持有锁的线程解锁,就会出现这种情况。当一个线程试图锁定一个自旋锁但没有成功时,它会不断地重新尝试锁定它,直到最终成功;因此,它将不允许另一个线程取代它的位置(然而,操作系统将强制切换到另一个线程,当然,一旦当前线程的CPU运行时量已经超过)。
这个问题
互斥锁的问题是让线程进入睡眠状态和再次唤醒它们都是相当昂贵的操作,它们需要相当多的CPU指令,因此也需要一些时间。如果互斥锁现在只被锁定了很短的一段时间,那么使线程进入睡眠状态并再次唤醒它所花费的时间可能会超过线程目前实际处于睡眠状态的时间,甚至可能超过线程不断轮询自旋锁所浪费的时间。另一方面,轮询自旋锁会不断地浪费CPU时间,如果锁持有的时间较长,这将浪费更多的CPU时间,如果线程处于睡眠状态会更好。
解决方案
在单核/单CPU系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞了唯一可用的CPU核心,就没有其他线程可以运行,而且由于没有其他线程可以运行,锁也不会被解锁。低,自旋锁只在这些系统上浪费CPU时间,没有真正的好处。如果将线程置于睡眠状态,另一个线程可能同时运行,可能会解锁锁,然后允许第一个线程在再次唤醒时继续处理。
在多核/多cpu系统上,如果大量的锁只被持有很短的时间,那么不断地让线程进入睡眠状态并再次唤醒它们所浪费的时间可能会显著降低运行时性能。当使用自旋锁时,线程有机会利用它们的全部运行时量(总是只阻塞很短的一段时间,但随后立即继续它们的工作),从而获得更高的处理吞吐量。
这种做法
由于程序员经常无法事先知道互斥锁或自旋锁哪个更好(例如,因为目标架构的CPU核数是未知的),操作系统也无法知道某段代码是针对单核环境还是多核环境进行了优化,大多数系统并没有严格区分互斥锁和自旋锁。事实上,大多数现代操作系统都有混合互斥锁和混合自旋锁。这到底是什么意思呢?
混合互斥起初在多核系统上表现得像自旋锁。如果一个线程不能锁定互斥锁,它不会立即进入睡眠状态,因为互斥锁可能很快就会被解锁,所以互斥锁首先会表现得完全像一个自旋锁。只有在一段时间后(或重试或任何其他测量因素)仍然没有获得锁时,线程才真正进入睡眠状态。如果相同的代码运行在一个只有单核的系统上,互斥锁将不会自旋锁,尽管如上所述,这并没有什么好处。
混合自旋锁一开始表现得像普通的自旋锁,但为了避免浪费太多CPU时间,它可能有一个后退策略。它通常不会让线程进入睡眠状态(因为在使用自旋锁时不希望发生这种情况),但它可能决定停止线程(要么立即停止,要么在一段时间后停止;这被称为“让步”),并允许另一个线程运行,从而增加了自旋锁被解锁的机会(你仍然有线程切换的成本,但没有让线程休眠并再次唤醒它的成本)。
总结
如果有疑问,使用互斥锁,它们通常是更好的选择,大多数现代系统将允许它们在很短的时间内自旋锁,如果这似乎是有益的。使用自旋锁有时可以提高性能,但只有在某些条件下,而且您有疑问的事实告诉我,您目前没有在任何项目中使用自旋锁可能是有益的。你可以考虑使用你自己的“锁对象”,它可以在内部使用自旋锁或互斥锁(例如,在创建这样的对象时,这种行为可以配置),最初在任何地方都使用互斥锁,如果你认为在某个地方使用自旋锁可能真的有帮助,可以尝试一下并比较结果(例如使用分析器),但一定要在你得出结论之前测试单核和多核系统(可能是不同的操作系统)。如果你的代码是跨平台的)。
实际上并不是iOS特有的,但iOS是大多数开发者可能面临的问题:如果你的系统有一个线程调度器,它不能保证任何线程,无论它的优先级有多低,最终都有机会运行,那么自旋锁可能会导致永久的死锁。iOS调度器区分不同的线程类,低级别的线程只有在高级别的线程不想运行时才会运行。这是没有退步策略的,所以如果您永远有高级线程可用,低级线程将永远不会获得任何CPU时间,因此永远没有机会执行任何工作。
问题如下:您的代码在一个低优先级线程中获得了一个自旋锁,当它处于该锁的中间时,时间量已经超过,线程停止运行。再次释放自旋锁的唯一方法是低优先级线程再次获得CPU时间,但这并不保证会发生。你可能有两个高优先级线程,它们总是想要运行,任务调度程序总是会优先考虑这些线程。它们中的一个可能会碰到自旋锁并试图获得它,当然这是不可能的,系统会让它屈服。问题是:一个屈服的线程可以立即再次运行!拥有比持有锁的线程更高的优先级,持有锁的线程就没有机会获得CPU运行时。要么是其他线程获得运行时,要么是刚刚产生的线程。
为什么互斥对象没有出现这个问题?当高优先线程无法获得互斥锁时,它不会屈服,它可能会旋转一点,但最终会被送入睡眠状态。一个睡眠线程在被一个事件唤醒之前是不可运行的,例如,一个事件,比如它一直在等待的互斥锁被解锁。Apple已经意识到这个问题,并因此弃用了OSSpinLock。新锁被称为os_unfair_lock。这个锁避免了上面提到的情况,因为它知道不同的线程优先级类。如果你确定在你的iOS项目中使用自旋锁是一个好主意,那就使用它。远离OSSpinLock!在任何情况下都不要在iOS中实现你自己的自旋锁!如果有疑问,使用互斥锁。macOS不受此问题影响,因为它有一个不同的线程调度程序,不允许任何线程(即使是低优先线程)“干运行”。在CPU时间上,仍然会出现同样的情况,然后会导致非常糟糕的性能,因此OSSpinLock在macOS上也被弃用了。
OSSpinLock
os_unfair_lock
继续Mecki的建议,这篇文章Pthread互斥与Pthread自旋锁在Alexander Sandler的博客上,Alex on Linux展示了spinlock &mutexes可以使用#ifdef来测试行为。
spinlock
mutexes
但是,一定要根据你的观察来做最后的决定,理解正如所举的例子是一个孤立的案例,你的项目要求、环境可能完全不同。
windows系统上的调度信息: http://download.microsoft.com/download/e/b/a/eba1050f-a31d-436b-9281-92cdfeae4b45/IRQL_thread.doc < / p >
自旋锁和互斥锁同步机制现在非常常见。
让我们首先考虑Spinlock。
基本上,它是一个忙碌的等待操作,这意味着我们必须等待指定的锁被释放,然后才能继续进行下一个操作。概念上很简单,但实现起来却不是那么回事。例如:如果锁还没有释放,那么线程已经被换出并进入睡眠状态,我们应该处理它吗?当两个线程同时请求访问时,如何处理同步锁?
通常,最直观的想法是通过一个变量来处理同步,以保护临界区。互斥锁的概念是相似的,但它们仍然是不同的。重点关注:CPU利用率。自旋锁需要消耗CPU时间来等待执行操作,因此,我们可以总结两者之间的差异:
在同构多核环境中,如果在临界区花费的时间较小,则使用Spinlock,因为我们可以减少上下文切换时间。(单核比较不重要,因为有些系统在中间实现Spinlock开关)
在Windows中,使用Spinlock会将线程升级到DISPATCH_LEVEL,这在某些情况下可能是不允许的,所以这次我们必须使用互斥锁(APC_LEVEL)。
梅基的回答非常准确。然而,在单个处理器上,当任务等待中断服务例程提供的锁时,使用自旋锁可能是有意义的。中断将控制权传递给ISR, ISR将准备好资源供等待任务使用。在将控制权交还给被中断的任务之前,它将以释放锁结束。旋转任务将发现自旋锁可用并继续。
在单核/单CPU系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞了唯一可用的CPU核心,就没有其他线程可以运行,而且由于没有其他线程可以运行,锁也不会被解锁。低,自旋锁只在这些系统上浪费CPU时间,没有真正的好处
这是错误的。在单处理器系统上使用自旋锁不会浪费cpu周期,因为一旦一个进程使用了自旋锁,抢占就被禁用了,因此,就不会有其他进程自旋了!只是使用它没有任何意义!因此,在Uni系统上的自旋锁被内核在编译时用preempt_disable代替!
使用自旋锁的规则很简单:当且仅当锁持有的实际时间有界且足够小时使用自旋锁。
注意,通常用户实现的自旋锁不满足这个要求,因为它们不禁用中断。除非禁用了抢占,否则在自旋锁进行时进行抢占违反了有限时间要求。
足够小是一种判断,取决于上下文。
例外:一些内核编程即使在时间没有限制的情况下也必须使用自旋锁。特别是如果CPU没有工作要做,它别无选择,只能旋转,直到有更多的工作出现。
特殊危险:在低级编程中,当存在多个中断优先级时要非常小心(通常至少有一个不可屏蔽中断)。在这种情况下,即使线程优先级上的中断被禁用(例如优先级硬件服务,通常与虚拟内存管理有关),高优先级的抢占也可以运行。如果保持严格的优先级分离,必须放宽有界实时的条件,并在该优先级级别上用有界系统时间代替。注意,在这种情况下,不仅锁持有者可以被抢占,旋转器也可以被中断;这通常不是问题,因为你对此无能为力。
自旋锁实际上可以在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)当使用具有多个线程的第三方库时,请记住他们可能没有阅读此讨论,可能会出错!