自旋锁与信号量


Answers:


134

自旋锁和信号灯的主要区别在于四点:

1.他们是什么
一个自旋锁是一个可能实现的锁,即一个由忙等待(“旋转”)来实现。信号量是锁的概括(或者相反,锁是信号量的特例)。通常,但不一定,自旋锁仅在一个进程内有效,而信号量也可用于在不同进程之间进行同步。

锁可以互斥,即一次一个线程可以获取该锁并继续执行代码的“关键部分”。通常,这意味着修改一些由多个线程共享的数据的代码。
一个信号有一个计数器,并允许自己被收购一个或多个线程,这取决于你的价值张贴到它的东西,(在一些实现),这取决于它的最大允许值是什么。

就此而言,可以认为锁是信号量的特殊情况,最大值为1。

2.他们的工作
如上所述,自旋锁是一种锁,因此是一种互斥(严格从1到1)的机制。它通常通过原子方式反复查询和/或修改内存位置来工作。这意味着获取自旋锁是“忙碌”的操作,可能长时间(甚至永远!)消耗CPU周期,而实际上却无法实现“空”。
这种方法的主要诱因是这样一个事实,即上下文切换的开销相当于旋转几百(或数千)次,因此,如果可以通过燃烧几个循环来获得锁,那么总体来说可能是更高效。同样,对于实时应用程序,阻塞并等待调度程序在将来某个遥远的时间返回它们可能也是不可接受的。

相比之下,信号量根本不旋转,或者仅旋转很短的时间(作为避免syscall开销的优化)。如果无法获取信号量,它将阻塞,从而将CPU时间浪费在准备运行的其他线程上。当然,这可能意味着在重新安排线程之前要经过几毫秒,但是如果这没问题(通常不是问题),那么它可能是一种非常有效的,CPU节约的方法。

3.它们在出现拥塞时的行为
常见的误解是自旋锁或无锁算法通常“速度更快”,或者仅对“非常短的任务”有用(理想情况下,不应将同步对象保留更长时间)绝对没有必要)。
一个重要的区别是存在拥塞时不同方法的行为方式。

设计良好的系统通常拥塞少或没有拥塞(这意味着并非所有线程都试图在同一时间获取锁)。例如,通常不会编写获取锁的代码,然后从网络加载半兆字节的zip压缩数据,对数据进行解码和解析,最后修改共享引用(将数据追加到容器等)。释放锁之前。取而代之的是,仅出于访问共享资源的目的而获得该锁。
由于这意味着关键部分之外的工作要多于关键部分内部的工作,因此自然而然地,在关键部分内部的线程的可能性相对较低,因此很少有线程同时争用锁。当然,偶尔会有两个线程尝试同时获取锁(如果不可能的话,您就不需要锁!),但这是一个例外,而不是“健康”系统中的规则。

在这种情况下,自旋锁的性能大大优于信号量,因为如果没有锁拥塞,则获取自旋锁的开销仅为十几个周期,而上下文切换则为数百/数千个周期,而丢失则为10-20百万个周期时间片的其余部分。

另一方面,由于拥塞程度很高,或者如果锁被长时间持有(有时您无能为力!),自旋锁将消耗大量的CPU周期以至于什么也没做。
在这种情况下,信号灯(或互斥锁)是更好的选择,因为它允许不同的线程在这段时间内运行有用的任务。或者,如果没有其他线程有用,则它允许操作系统节流CPU并减少热量/节省能量。

此外,单核系统上,一个自旋锁会在锁拥塞存在相当低效,作为纺丝线会浪费的状态改变是不可能发生的(直到释放线程调度,其中其完整的时间等待ISN等待线程正在运行时不会发生!)。因此,在有任何争用的情况下,在最佳情况下获取锁大约需要1 1/2个时间片(假设释放线程是正在计划的下一个线程),这不是很好的行为。

4.如何实现
如今,信号量通常会sys_futex在Linux下包装(可选地,自旋锁会在几次尝试后退出)。
自旋锁通常使用原子操作来实现,而不使用操作系统提供的任何操作。过去,这意味着使用编译器内部函数或非便携式汇编程序指令。同时,C ++ 11和C11都将原子操作作为语言的一部分,因此,除了编写可证明正确的无锁代码的一般困难之外,现在有可能在完全可移植且(几乎)可移植的代码中实现无锁代码。无痛的方法。


“此外,在单核系统上,自旋锁在存在锁拥塞的情况下效率很低,因为自旋线程会浪费其全部时间来等待不可能发生的状态更改”:还有(至少在Linux上)spin_trylock,如果无法获取该锁,则会立即返回错误代码。自旋锁并不总是那么苛刻。但是spin_trylock对于一个应用程序,使用要求以这种方式进行适当的设计(可能是一个待处理操作队列,在这里,选择下一个操作,将实际操作留在队列中)。
Hibou57年

阻塞互斥量和信号量不仅在单线程环境中有用,而且在存在超额订购的情况下也有用,也就是说,一个程序(或共享系统的多个程序)创建的线程数大于硬件资源的数目。在这些情况下,阻塞线程可以使其他线程能够以有用的方式使用CPU时间。另外,如果硬件支持超线程,则另一个线程可以利用用于执行空闲循环的执行单元。
豪尔赫·贝隆

76

很简单,一个信号量是一个“收益”同步对象,一个自旋锁是一个“繁忙等待”对象。(信号量还有很多其他之处,因为它们可以同步多个线程,这与互斥锁或保护或监视器或关键部分不同,后者可以保护代码区域免受单个线程的攻击)

您可能会在更多情况下使用信号量,但在极短的时间内要锁定的地方使用自旋锁-锁定的成本较高,尤其是锁定很多时。在这种情况下,等待受保护的资源被解锁时,自旋锁会更有效。如果旋转时间过长,显然会降低性能。

通常,如果旋转的时间长于线程量子的时间,则应使用信号量。


27

除了Yoav Aviram和gbjbaanb所说的以外,另一个关键点过去是,您永远不会在单CPU机器上使用自旋锁,而在这种机器上使用信号量是有意义的。如今,您经常很难找到没有多核,超线程或同等功能的机器,但是在只有一个CPU的情况下,应该使用信号量。(我相信原因很明显。如果单个CPU忙于等待其他操作来释放自旋锁,但它正在唯一的CPU上运行,则在当前进程或线程被以下命令抢占之前,该锁不大可能被释放O / S,这可能需要一段时间,并且在抢占之前没有任何用处。)


7
我想说明一下,在单线程系统上不使用自旋锁有多么重要。它们是优先级反转问题的最重要标志。相信我:您不想调试这些错误。
Nils Pipenbrinck

2
自旋锁在Linux内核中无处不在,无论您是否拥有一个或多个CPU。你到底是什么意思
法肯教授2010年

@Amigable:根据定义,自旋锁意味着CPU上的当前线程正在等待其他东西释放锁定的对象。如果唯一可以更改锁的活动对象是当前CPU,则不会通过旋转释放锁。如果还有其他事情-DMA传输或其他I / O控制器可以很好地释放锁。但是在没有其他东西可以释放锁的情况下进行旋转不是很明智-您最好将CPU交给另一个进程,就像等待被抢占一样。
乔纳森·莱夫勒

1
我可能很错,但是给人的印象是可重入(单CPU)Linux内核可能会中断正在运行的自旋锁。
法肯教授2010年

2
@Amigable:我也有可能错了,但我认为我接近自旋锁的经典定义。使用抢先式调度,进程可能会旋转锁直到其时间片结束,或者直到中断导致它屈服为止,但是如果另一个进程必须提供允许自旋锁锁定的条件,则自旋锁不会在单个CPU机器上的好主意。我正在研究的系统具有自旋锁,并且在进入非繁忙等待模式之前,对自旋数具有可配置的上限。这是用户级别的自旋锁;内核可能有所不同。
乔纳森·莱夫勒

19

来自Rubinni的Linux设备驱动程序

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


8

我不是内核专家,但有几点:

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

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

基本上,如果临界区很小(小于睡眠/唤醒的开销),并且临界区没有调用任何可以睡眠的东西,则应使用自旋锁!如果关键部分较大并且可以入睡,则应使用信号量。

拉曼·查洛特拉。


7

自旋锁是指使用依赖于机器的汇编指令(例如,测试并设置)实现线程间锁定的实现。之所以称为自旋锁,是因为线程只是简单地在循环中等待(“自旋”),反复检查直到锁变为可用(繁忙等待)。自旋锁可以用作互斥锁的替代品,互斥锁是操作系统(不是CPU)提供的一种功能,因为自旋锁如果锁定时间较短,性能会更好。

信号量是操作系统为IPC提供的功能,因此它的主要目的是进程间通信。作为操作系统提供的一种工具,它的性能将不如用于跨轴锁定的自旋锁的性能好(尽管可能)。信号量更适合长时间锁定。

就是说-在组装中实现夹板是棘手的,而且不便于移植。


4
所有多线程CPU都需要一个自旋锁指令(“测试并设置”),并且在硬件中始终将其实现为单条指令,因为否则总会出现竞争状态,其中有多个线程认为它“拥有”了受保护的资源。
理查德·T

我不确定您是否理解信号量...请参阅Dijkstra所说的话:cs.cf.ac.uk/Dave/C/node26.html
gbjbaanb

POSIX区分线程共享的信号量和进程共享的信号量。
格雷格·罗杰斯

2
信号量用于进程间同步,而不用于通信。
Johan Bezem 2011年

6

我想补充一下我的看法,这些看法比较笼统,但不是特定于Linux。

根据内存体系结构和处理器功能的不同,您可能需要自旋锁才能在多核或多处理器系统上实现信号量,因为在此类系统中,当两个或多个线程/进程需要一个或多个线程/进程时,可能会发生竞争状态获取信号量。

是的,如果您的内存体系结构通过一个内核/处理器延迟了所有其他访问来锁定某个内存部分,并且如果您的处理器提供了测试并设置,则可以实现没有自旋锁的信号灯(但要非常小心! )。

但是,由于设计了简单/廉价的多核系统(我在嵌入式系统中工作),所以并非所有的内存架构都支持这种多核/多处理器功能,仅支持测试设置或等效功能。那么一个实现可以如下:

  • 获取自旋锁(忙于等待)
  • 尝试获取信号量
  • 释放自旋锁
  • 如果未成功获取信号灯,则挂起当前线程,直到释放信号灯;否则继续关键部分

释放信号量将需要执行以下步骤:

  • 获得自旋锁
  • 释放信号量
  • 释放自旋锁

是的,对于OS级别上的简单二进制信号量,可以仅使用自旋锁作为替换。但是,只有要保护的代码段确实很小时,才可以。

如前所述,如果以及当您实现自己的操作系统时,请务必小心。调试这样的错误很有趣(我认为,很多人并不认同),但大多数情况下非常繁琐且困难。


1

“互斥锁”(或“互斥锁”)是两个或多个异步进程可以用来保留共享资源以供独占使用的信号。获得“互斥体”所有权的第一个过程也获得共享资源的所有权。其他进程必须等待第一个进程释放对“互斥体”的所有权,然后才能尝试获取它。

内核中最常见的锁定原语是自旋锁。自旋锁是一个非常简单的单持有人锁。如果某个进程尝试获取自旋锁,但该锁不可用,则该进程将继续尝试(自旋)直到可以获取该锁。这种简单性创建了一个小型且快速的锁。


1

当且仅当您非常确定预期结果将在线程的执行切片时间到期之前不久发生时,才使用Spinlock。

示例:在设备驱动程序模块中,驱动程序在硬件寄存器R0中写入“ 0”,现在它需要等待该R0寄存器变为1。H/ W读取R0并执行一些工作,然后在R0中写入“ 1”。这通常很快(以微秒为单位)。现在旋转比睡觉和被硬件打断要好得多。当然,在旋转时,需要注意硬件故障条件!

用户应用程序绝对没有旋转的理由。这没有道理。您将旋转发生某个事件,并且该事件需要由另一个用户级应用程序完成,而该应用程序永远不能保证在快速的时间内发生。因此,我不会在用户模式下旋转。我最好在用户模式下进入sleep()或mutexlock()或信号锁定()。


1

什么是自旋锁和信号量之间的区别?Maciej Piechotka撰写

两者都管理有限的资源。我将首先描述二进制信号量(互斥体)和自旋锁之间的区别。

自旋锁执行繁忙的等待-即它保持运行循环:

while(try_acquire_resource()); 
 ...  
释放();

它执行非常轻量级的锁定/解锁,但是如果锁定线程被其他线程抢占(将尝试访问相同的资源),则第二个线程将简单地尝试释放资源,直到耗尽CPU数量。
另一方面,互斥体的行为更像:

如果(!try_lock()){
    add_to_waiting_queue();
    等待();
}
...
进程* p = get_next_process_from_waiting_queue();
p-> wakeUp();

因此,如果线程尝试获取被阻止的资源,它将被挂起,直到对它可用为止。锁定/解锁要沉重得多,但是等待是“自由”和“公平”的。

信号量是一种允许多次使用(从初始化开始就知道)的锁-例如,允许3个线程同时保存资源,但不得多。例如,它用于生产者/消费者问题或一般在队列中使用:

P(资源_sem)
资源= resources.pop()
...
resources.push(资源)
V(resources_sem)

信号量,互斥锁和自旋锁之间的区别?

锁定Linux


1
似乎是它的复制/粘贴;-):自旋锁和信号灯有什么区别?
Hibou57

0

自旋锁只能由一个进程保持,而信号量可以由一个或多个进程保持。旋转锁等待,直到进程释放锁,然后获取锁。信号量处于睡眠锁定状态,即等待并进入睡眠状态。

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.