自旋锁与信号量

131

信号量和自旋锁有哪些基本区别?

在什么情况下会选择使用信号量而不是自旋锁?

11个回答

154

自旋锁和信号量主要有四个不同之处:

1. 它们的定义
自旋锁是锁的一种实现方式,它是通过忙等待(“自旋”)来实现的。 信号量是锁的一般化(或者反过来说,锁是信号量的一种特殊情况)。通常,自旋锁仅在一个进程内有效,而信号量也可以用于不同进程之间的同步。但不是必须如此

锁的作用在于互斥,即一次只有一个线程可以获取锁并继续执行“临界区”代码。通常,这意味着修改一些由多个线程共享的数据的代码。
信号量有一个计数器,并允许一个或多个线程获取它,这取决于您发布给它的值以及(在某些实现中)其最大允许值。

因此,可以将锁视为最大值为1的信号量的特例。

2. 它们的作用
如上所述,自旋锁是一种锁,因此是一种互斥(严格1对1)机制。它通过重复查询和/或修改内存位置来工作,通常以原子方式。这意味着获取自旋锁是一种“繁忙”的操作,可能会消耗大量CPU周期的时间(甚至永远!),而实际上没有实现任何有用的操作。
采用这种方法的主要动机在于,上下文切换具有等价于自旋几百次(或者可能是几千次)的开销,因此如果可以通过自旋烧掉一些周期来获取锁,则可能总体上更为高效。对于实时应用程序,阻塞并等待调度程序在未来某个时间回到它们可能是不可接受的。

相比之下,信号量要么根本不旋转,要么仅旋转很短的时间(作为一种优化避免系统调用开销)。如果不能获取信号量,则会阻塞,让出CPU时间给另一个准备运行的线程。当然,这可能意味着在您的线程再次被安排之前会经过几毫秒的时间,但如果这没有问题(通常情况下不会有问题),那么它可以是一种非常有效、节约CPU的方法。
3.它们在拥堵状态下的行为 普遍误解自旋锁或无锁算法“通常更快”,或者它们仅对“非常短的任务”有用(理想情况下,永远不应该持有任何同步对象比必要时间更长)。 一个重要区别是不同方法在“拥堵状态下”的行为方式。
一个设计良好的系统通常具有低或无拥堵(这意味着并非所有线程都尝试在完全相同的时间获取锁)。例如,通常不会编写代码来获取锁,然后从网络加载500KB的压缩数据,解码和解析数据,最后修改共享的引用(将数据附加到容器等)然后才释放锁定。而是仅为访问共享资源而获取锁定。 由于这意味着关键段之外有相当多的工作而不是内部工作,因此线程在关键段内的可能性自然较低,因此同时争夺锁定的线程很少。当然,偶尔会有两个线程同时尝试获取锁(如果这是不可能发生的,则不需要锁!),但这在“健康”的系统中更像是例外而不是规则。
在这种情况下,如果没有锁定拥堵,旋转锁明显优于信号量,因为如果没有锁定拥堵,获取旋转锁的开销仅为几十个周期,而上下文切换需要数百/数千个周期,失去时间片的剩余部分需要1000-2000万个周期。

另一方面,如果出现高度拥塞的情况,或者锁被长时间持有(有时候你就是无法避免!),自旋锁会烧掉大量的CPU周期却毫无作用。


在这种情况下,信号量(或互斥锁)是一个更好的选择,因为它允许另一个线程在此期间运行有用的任务。或者,如果没有其他线程有什么有用的事情要做,它允许操作系统减弱CPU并降低热量/节约能源。

而且,在单核系统上,当存在锁拥塞时,自旋锁会相当低效,因为自旋线程将浪费其所有时间等待一个状态改变,而这个状态改变不可能发生(直到释放线程被调度,但在等待线程运行时,这种情况 不会发生!)。因此,即使存在任何争用,获取锁也需要大约1个半时间片(假设释放线程是下一个被调度的线程),这并不是很好的行为。

4. 它们如何实现
在Linux下,信号量现在通常使用sys_futex进行包装(可选择使用自旋锁,在几次尝试后退出)。
自旋锁通常使用原子操作实现,而不使用操作系统提供的任何内容。过去,这意味着使用编译器内置函数或非可移植的汇编指令。与此同时,C++11和C11都将原子操作作为语言的一部分,因此除了编写可证明正确的无锁代码的一般难度外,现在完全可以以一种完全可移植且(几乎)不痛苦的方式实现无锁代码。


此外,在单核系统上,当锁拥塞时,自旋锁会非常低效,因为自旋线程将浪费其全部时间等待一个不可能发生的状态更改。在Linux上还有spin_trylock,如果无法获取锁,则立即返回错误代码。自旋锁并不总是那么严格。但是,使用spin_trylock需要应用程序以正确的方式进行设计(可能是挂起操作的队列,并在此处选择下一个操作,将实际操作留在队列中)。 - Hibou57
阻塞互斥锁和信号量不仅在单线程环境中有用,而且在存在超额订阅的情况下也很有用。也就是说,程序(或共享系统的多个程序)创建的线程数高于硬件资源的数量。在这些情况下,阻塞您的线程允许其他线程以有用的方式使用CPU时间。此外,如果硬件支持超线程,其他线程可以利用正在执行空闲循环的执行单元。 - Jorge Bellon

79
非常简单地说,信号量是一种“放弃”同步对象,自旋锁则是一种“忙等待”的同步对象。(信号量有一些额外的功能,能够同步多个线程,不像互斥锁、保护对象、监视器或关键段只能保护代码区域免于被单个线程访问。)
在更多情况下,您会使用信号量,但在需要锁定的时间非常短暂的情况下,则可以使用自旋锁。锁定的成本很高,特别是如果经常进行锁定。在这种情况下,等待受保护资源解锁时,自旋锁可以更有效地自旋一段时间。显然,如果旋转时间过长,则会影响性能。
通常,如果自旋时间超过线程时间片,那么应该使用信号量。

28

除了Yoav Aviram和gbjbaanb所说的内容之外,以前的另一个关键点是在单CPU机器上永远不会使用自旋锁(spin-lock),而在这种情况下使用信号量(semaphore)是有意义的。现在,很难找到没有多个核心、超线程或等效功能的机器,但在只有一个CPU的情况下,应该使用信号量。(我相信原因是显而易见的。如果单个CPU正忙于等待其他东西释放自旋锁,但它正在唯一的CPU上运行,则在当前进程或线程被操作系统(preemption)抢占之前,锁不太可能被释放,这可能需要一段时间,并且在抢占发生之前不会发生任何有用的事情。)


7
我同意不在单线程系统中使用自旋锁的重要性。它们是优先级反转问题的导火索。相信我,您不想调试此类错误。 - Nils Pipenbrinck
2
Linux内核中存在着大量的自旋锁,无论你有一个还是多个CPU。你的意思是什么? - Prof. Falken
1
我可能错了,但我的印象是可重入(单CPU)Linux内核可以中断正在运行的自旋锁。 - Prof. Falken
2
@Amigable:我可能也错了,但我认为我接近自旋锁的经典定义。在抢占式调度中,一个进程可能会在锁上自旋,直到它的时间片结束,或者直到中断导致它放弃,但如果另一个进程必须提供允许自旋锁锁定的条件,则自旋锁不适用于单CPU机器。我所在的系统具有自旋锁,并且具有可配置的自旋次数上限,超过该上限后将进入非忙等待模式。这是用户级自旋锁;在内核中可能存在差异。 - Jonathan Leffler
抱歉,我给你的回答点了踩 - 虽然从性能角度来看自旋锁可能不是一个好主意,但我所知道的几乎所有现代系统(无论是单CPU还是多CPU、服务器还是嵌入式)都使用某种非协作式多任务处理,其中阻塞任务不必手动让出。因此,一般说自旋锁不能在单CPU上使用是不正确的。 - MikeMB
显示剩余4条评论

20

来自Rubinni的《Linux设备驱动程序》

与信号量不同,自旋锁可用于不能睡眠的代码中,例如中断处理程序。


8

我不是内核专家,但以下是一些要点:

即使是单处理器机器,在编译内核时启用内核抢占也可以使用自旋锁。如果禁用内核抢占,则自旋锁(可能)扩展为void语句。

此外,当我们尝试比较信号量和自旋锁时,我认为信号量是指内核中使用的信号量,而不是用于IPC(用户空间)的信号量。

基本上,如果关键部分很小(小于睡眠/唤醒的开销),且关键部分不调用可能会导致休眠的任何内容,则应使用自旋锁。如果关键部分更大且可能会休眠,则应使用信号量。

Raman Chalotra。


7
自旋锁是使用与机器相关的汇编指令(如测试和设置)实现线程间锁定的一种方式。它被称为自旋锁,因为线程只需在一个循环中等待(“旋转”),不断检查直到锁可用为止(忙等待)。自旋锁常用作互斥锁的替代品,互斥锁是由操作系统(而非CPU)提供的设施,但如果锁定时间短,则自旋锁的性能更好。
信号量是操作系统提供的用于IPC的设施,因此其主要目的是进程间通信。作为操作系统提供的设施,它的性能在进行线程间锁定时不如自旋锁(尽管可能有可能)。信号量更适合长时间锁定。
话虽如此-在汇编中实现自旋锁很棘手,而且不具有可移植性。

4
所有支持多线程的CPU都需要自旋锁指令(“测试与设置”),并且它通常在硬件中作为单个指令实现,因为否则就会出现竞态条件,导致多个线程都认为自己“拥有”受保护资源。 - Richard T
我不确定你是否理解信号量...看看Dijkstra说了什么:http://www.cs.cf.ac.uk/Dave/C/node26.html - gbjbaanb
POSIX区分线程共享的信号量和进程共享的信号量。 - Greg Rogers
2
信号量用于进程间同步,而不是通信。 - Johan Bezem

6
我想加入我的观察,更普遍且不太与Linux相关。
根据内存结构和处理器能力,您可能需要在多核或多处理器系统上实现旋转锁以实现信号量,因为在这种系统中,当两个或多个线程/进程要获取信号量时可能会发生竞争条件。
是的,如果您的内存架构通过一个核心/处理器延迟所有其他访问来锁定内存部分,并且您的处理器提供测试和设置,则可以非常小心地实现旋转锁(但非常小心!)信号量。
然而,由于简单/廉价的多核系统被设计出来(我正在从事嵌入式系统),并不是所有内存架构都支持这样的多核/多处理器特征,只支持测试和设置或等效功能。那么实现如下:
- 获取旋转锁(繁忙等待) - 尝试获取信号量 - 释放旋转锁 - 如果未成功获取信号量,请暂停当前线程,直到信号量被释放;否则继续进行关键部分
释放信号量需要按以下方式实现:
- 获取旋转锁 - 释放信号量 - 释放旋转锁
是的,在操作系统级别上,对于简单的二进制信号量,仅使用旋转锁也可能是可能的,但前提是要保护的代码部分确实非常小。
如前所述,如果您自己实现操作系统,请务必小心。调试此类错误很有趣(我个人认为,不被许多人共享),但通常非常繁琐和困难。

1

自旋锁只能被一个进程持有,而信号量可以被一个或多个进程持有。 自旋锁会等待进程释放锁,然后再获取锁。 信号量是睡眠锁,即等待并进入睡眠状态。


1

Spinlock只有在您非常确定您的预期结果很快就会发生,即在您的线程执行时间片到期之前使用。

例如:在设备驱动程序模块中,驱动程序在硬件寄存器R0中写入“0”,现在需要等待该R0寄存器变为1。硬件读取R0并进行一些工作,然后在R0中写入“1”。这通常很快(在微秒内)。现在旋转比睡眠好得多,并且不会被硬件中断。当然,在旋转时,需要注意硬件故障条件!

用户应用程序绝对没有旋转的理由。这没有意义。您将旋转以等待某个事件发生,而该事件需要由另一个用户级应用程序完成,这永远不能保证在短时间内发生。因此,在用户模式下我不会旋转。我更喜欢在用户模式下使用sleep()、mutexlock()或semaphore lock()。


1

来自Maciej Piechotka自旋锁和信号量之间有什么区别?

两者都在管理有限资源。我首先会描述二元信号量(互斥锁)和自旋锁之间的区别。
自旋锁执行忙等待 - 也就是说,它会一直运行循环:
while (try_acquire_resource ());
它执行非常轻量级的锁定/解锁,但如果锁定线程被其他线程抢占并试图访问相同的资源,则第二个线程将简单地尝试获取资源,直到它耗尽CPU时间片。
另一方面,互斥锁的行为更像是:
if (!try_lock()) {
    add_to_waiting_queue ();
    wait();
}
process *p = get_next_process_from_waiting_queue ();
p->wakeUp ();
因此,如果线程尝试获取被阻塞的资源,它将被挂起,直到它可用。 锁定/解锁更加繁重,但等待是“免费”和“公平”的。
信号量是一个允许多次使用(从初始化中知道)的锁 - 例如允许3个线程同时持有该资源,但不允许更多。 它通常用于生产者/消费者问题或普遍的队列中:
P(resources_sem)
resource = resources.pop()
...
resources.push(resources)
V(resources_sem)

信号量、互斥锁和自旋锁的区别?

Linux中的锁定机制


1
似乎是复制/粘贴这个;-):自旋锁和信号量之间有什么区别?(http://unix.stackexchange.com/questions/5214/what-is-the-difference-between-spin-locks-and-semaphores) - Hibou57

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