我认为两者都完成了相同的工作,您如何决定使用哪个进行同步?
我认为两者都完成了相同的工作,您如何决定使用哪个进行同步?
理论
在理论上,当一个线程尝试锁定互斥锁但失败时(因为该锁已经被锁定),它会进入睡眠状态,立即允许另一个线程运行。它将继续睡眠,直到被唤醒,这将在之前持有锁的任何线程解锁该互斥锁时发生。当一个线程尝试锁定自旋锁但失败时,它将不断尝试锁定它,直到最终成功为止;因此它不会允许另一个线程替代它(然而,当当前线程的 CPU 运行时间量已超过时,操作系统将强制切换到另一个线程)。
问题
互斥锁的问题在于,将线程置于睡眠状态并将它们唤醒的操作都是非常昂贵的操作,它们需要相当多的 CPU 指令,因此也需要一些时间。如果现在互斥锁只被锁定了很短的时间,那么将一个线程置于睡眠状态并唤醒它所花费的时间可能会远远超过线程实际睡眠的时间,并且甚至可能超过线程通过不断轮询自旋锁浪费的时间。另一方面,在自旋锁上轮询将不断浪费 CPU 时间,如果锁被持有了较长的时间,这将浪费更多的 CPU 时间,如果线程睡眠会更好。
解决方案
在单核/单 CPU 系统上使用自旋锁通常没有意义,因为只要自旋锁轮询阻塞了唯一可用的 CPU 核心,就没有其他线程可以运行,因此也没有其他线程可以解锁该锁。换句话说,在这些系统上,自旋锁只是浪费 CPU 时间而没有实际好处。如果将线程置于睡眠状态,另一个线程可以立即运行,可能解锁该锁,然后在该线程再次唤醒后允许第一个线程继续处理。
在多核/多 CPU 系统上,当有许多锁仅被持有了很短的时间时,不断将线程置于睡眠状态并唤醒它们所浪费的时间可能会明显降低运行时性能。当使用自旋锁时,线程有机会利用其完整的运行时间量(始终只阻塞很短的时间混合互斥锁在多核系统上的行为类似于自旋锁。如果线程无法锁定互斥锁,它不会立即进入睡眠状态,因为互斥锁可能很快被解锁,因此互斥锁会首先表现得像自旋锁一样。只有在一定时间(或重试或任何其他测量因素)后仍未获得锁时,线程才会真正进入睡眠状态。如果相同的代码在只有一个核心的系统上运行,则互斥锁将不会自旋锁定,因为像上面所说,这并不是有利的。
根据Mecki的建议,这篇文章pthread mutex vs pthread spinlock在Alexander Sandler的博客Alex on Linux上展示了如何使用#ifdef实现spinlock
和mutexes
以测试它们的行为。
然而,请确保基于你的观察和理解做出最终决定,因为所给出的示例是一个孤立的案例,你的项目需求和环境可能完全不同。
Mecki的答案非常好。然而,在单处理器上,当任务正在等待中断服务例程给予锁时,使用自旋锁可能是有意义的。中断将控制传输到ISR,ISR将准备资源供等待任务使用。它将在将锁释放之前结束,并将控制权交还给被中断的任务。自旋任务将发现自旋锁可用并继续执行。
自旋锁在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微秒内获取此锁。因此,影响可能非常极端。