自旋锁与信号量

Posted

技术标签:

【中文标题】自旋锁与信号量【英文标题】:Spinlock versus Semaphore 【发布时间】:2010-09-16 18:54:18 【问题描述】:

信号量和自旋锁之间的基本区别是什么?

我们什么时候会在自旋锁上使用信号量?

【问题讨论】:

【参考方案1】:

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

【讨论】:

【参考方案2】:

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

【讨论】:

我想强调在单线程系统上不使用自旋锁的重要性。它们是优先级反转问题。相信我:你不想调试这些错误。 自旋锁在 Linux 内核中无处不在,无论您是否拥有一个或多个 CPU。你到底是什么意思? @Amigable:根据定义,自旋锁意味着 CPU 上的当前线程正在等待其他东西来释放锁定的对象。如果唯一可以更改锁的活动事物是当前 CPU,则不会通过旋转来释放锁。如果有别的东西——DMA 传输或其他 I/O 控制器可以释放锁,一切都很好。但是,当没有其他东西可以释放锁时旋转是不太明智的——你现在不如将 CPU 让给另一个进程,以等待被抢占。 我很可能错了,但我的印象是可重入(单 CPU)Linux 内核可能会中断正在运行的自旋锁。 @Amigable:我也有可能错了,但我认为我接近自旋锁的经典定义。使用抢占式调度,一个进程可能会在一个锁上旋转直到它的时间片结束,或者直到一个中断导致它屈服,但是如果另一个进程必须提供允许自旋锁锁定的条件,那么自旋锁不是在单 CPU 机器上的好主意。我工作的系统有自旋锁,并且在进入非忙等待模式之前,自旋次数有一个可配置的上限。这是一个用户级的自旋锁;内核中可能存在差异。【参考方案3】:

来自what is the difference between spin locks and semaphores?Maciej Piechotka:

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

Spin locks 执行忙等待 - 即它保持循环运行:

而(try_acquire_resource()); ... 释放();

它执行非常轻量级的锁定/解锁,但如果锁定线程将被其他尝试访问相同资源的线程抢占,第二个线程将简单地尝试获取资源,直到它用完 CPU 配额。 另一方面,互斥锁的行为更像:

如果(!try_lock()) add_to_waiting_queue (); 等待(); ... 进程 *p = get_next_process_from_waiting_queue (); p->唤醒();

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

Semaphore 是一个允许多次使用(从初始化中知道)次数的锁 - 例如,允许 3 个线程同时持有资源,但不能更多。例如,它用于生产者/消费者问题或一般用于队列:

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

Difference between semaphore, mutex & spinlock?

Locking in Linux

【讨论】:

似乎是这个的复制/粘贴;-):what is the difference between spin locks and semaphores?【参考方案4】:

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

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

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

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

拉曼查洛特拉。

【讨论】:

【参考方案5】:

自旋锁和信号量主要有四点不同:

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

锁用于互斥,即一次一个线程可以获取锁并继续执行代码的“关键部分”。通常,这意味着修改多个线程共享的某些数据的代码。semaphore 有一个计数器,并允许自己被 一个或多个 线程获取,具体取决于您发布给它的值,并且(在某些实现中)取决于它的最大允许值为。

到目前为止,可以将锁视为最大值为 1 的信号量的特殊情况。

2。他们做什么 如上所述,自旋锁是一种锁,因此是一种互斥(严格 1 对 1)机制。它通过重复查询和/或修改内存位置来工作,通常以原子方式。这意味着获取自旋锁是一个“忙”操作,可能会长时间(可能永远!)消耗 CPU 周期,而它实际上是“什么都没有”。 这种方法的主要动机是上下文切换的开销相当于旋转几百(或几千)次,因此如果可以通过燃烧几个旋转周期来获得锁,那么总体上很可能是更高效。此外,对于实时应用程序,阻止并等待调度程序在未来某个遥远的时间返回它们可能是不可接受的。

相比之下,信号量要么根本不旋转,要么只旋转很短的时间(作为避免系统调用开销的优化)。如果无法获取信号量,它会阻塞,将 CPU 时间让给准备好运行的不同线程。这当然可能意味着在您的线程再次被调度之前要经过几毫秒,但如果这没有问题(通常不是问题),那么它可能是一种非常高效、节省 CPU 的方法。

3.它们在拥堵情况下的行为方式 一种常见的误解是,自旋锁或无锁算法“通常更快”,或者它们只对“非常短的任务”有用(理想情况下,任何同步对象的保存时间都不应超过绝对必要的时间)。 一个重要的区别是不同的方法在拥塞的情况下表现如何

一个设计良好的系统通常拥塞很少或没有拥塞(这意味着并非所有线程都试图同时获取锁)。例如,通常不会编写获取锁的代码,然后从网络加载半兆字节的 zip 压缩数据,解码和解析数据,最后修改共享引用(附加数据到容器等),然后再释放锁。相反,获取锁只是为了访问共享资源。 由于这意味着在临界区之外的工作比在临界区内部的工作要多得多,因此线程在临界区内部的可能性自然相对较低,因此很少有线程同时竞争锁。当然,有时两个线程会尝试同时获取锁(如果这种情况不可能发生,你就不需要锁了!),但这是一个例外而不是在一个“健康”的系统中统治。

在这种情况下,自旋锁大大优于信号量,因为如果没有锁拥塞,获取自旋锁的开销只有十几个周期,而上下文切换或 10-2000 万个周期以丢失时间片的剩余部分。

另一方面,如果拥塞严重,或者如果锁被长时间持有(有时你就是忍不住!),自旋锁会消耗大量的 CPU 周期而一无所获。 在这种情况下,信号量(或互斥量)是更好的选择,因为它允许不同的线程在此期间运行 有用 任务。或者,如果没有其他线程可以做任何有用的事情,它允许操作系统降低 CPU 并减少热量/节约能源。

另外,在单核系统上,自旋锁在锁拥塞的情况下效率很低,因为自旋线程将浪费其全部时间等待不可能发生的状态更改(直到释放线程被调度,在等待线程运行时不会发生!)。因此,给定 任何 数量的争用,在最好的情况下获取锁大约需要 1 1/2 时间片(假设释放线程是下一个被调度的线程),这不是很好的行为。

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

【讨论】:

“另外,在单核系统上,自旋锁在锁拥塞的情况下效率很低,因为自旋线程将浪费其全部时间等待不可能发生的状态更改”:还有(至少在 Linux 上)spin_trylock,如果无法获取锁,它会立即返回错误代码。自旋锁并不总是那么苛刻。但是使用spin_trylock 需要以这种方式正确设计应用程序(可能是待处理操作的队列,在这里,选择下一个操作,将实际操作留在队列中)。 阻塞互斥锁和信号量不仅在单线程环境中有用,而且在超额订阅的情况下也有用,即一个程序(或共享系统的多个程序)创建的线程数高于硬件资源的数量。在这些情况下,阻塞您的线程允许其他人能够以有用的方式使用 CPU 时间。此外,如果硬件支持超线程,其他线程可以利用正在用于执行空闲循环的执行单元。【参考方案6】:

当且仅当您非常确定您的预期结果将在您的线程执行切片时间到期之前很快发生时才使用自旋锁。

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

用户应用程序绝对没有理由旋转。这没有意义。您将旋转以使某些事件发生,并且该事件需要由另一个用户级应用程序完成,而该应用程序永远不能保证在快速时间范围内发生。所以,我根本不会在用户模式下旋转。我最好在用户模式下 sleep() 或 mutexlock() 或 semaphore lock()。

【讨论】:

【参考方案7】:

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

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

【讨论】:

【参考方案8】:

来自 Rubinni 的 Linux 设备驱动程序

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

【讨论】:

【参考方案9】:

我想补充一下我的观察,更笼统,不是很针对 Linux。

根据内存架构和处理器功能,您可能需要自旋锁才能在多核或多处理器系统上实现信号量,因为在此类系统中,当两个或多个线程时可能会发生争用情况/processes 想要获取一个信号量。

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

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

获取自旋锁(忙于等待) 尝试获取信号量 释放自旋锁 如果没有成功获取信号量,则暂停当前线程,直到信号量被释放;否则继续关键部分

释放信号量需要按如下方式实现:

获取自旋锁 释放信号量 释放自旋锁

是的,对于操作系统级别的简单二进制信号量,可以只使用自旋锁作为替代。但前提是要保护的代码段非常小。

如前所述,如果您实施自己的操作系统,请务必小心。调试此类错误很有趣(我的看法,很多人不同意),但大多非常乏味和困难。

【讨论】:

【参考方案10】:

自旋锁是指使用机器相关的汇编指令(例如测试和设置)实现线程间锁定。它被称为自旋锁,因为线程只是在循环中等待(“自旋”)重复检查直到锁可用(忙等待)。自旋锁被用作互斥锁的替代品,互斥锁是操作系统(而不是 CPU)提供的一种工具,因为自旋锁在短时间锁定时性能更好。

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

也就是说 - 在汇编中实现 splinlock 很棘手,而且不可移植。

【讨论】:

所有多线程 CPU 都需要一个自旋锁指令(“测试和设置”),并且它总是在硬件中作为单条指令实现,因为否则总是会出现多个线程认为的竞争条件它“拥有”受保护的资源。 我不确定你是否理解信号量...看看 Dijkstra 怎么说:cs.cf.ac.uk/Dave/C/node26.html POSIX 区分线程共享的信号量和进程共享的信号量。 信号量用于进程间同步,而不是通信。【参考方案11】:

非常简单,信号量是“让出”同步对象,自旋锁是“忙等待”对象。 (信号量还有一点点需要同步多个线程,这与保护代码区域免受单个线程影响的互斥锁、守卫、监视器或临界区不同)

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

通常,如果您旋转的时间超过线程量子,那么您应该使用信号量。

【讨论】:

以上是关于自旋锁与信号量的主要内容,如果未能解决你的问题,请参考以下文章

本地自旋锁与信号量/多服务台自旋队列-spin wait风格的信号量

Linux 内核同步之自旋锁与信号量的异同

信号量,互斥锁,自旋锁

自旋锁spinlock解析

五自旋锁(spinlock)

Linux驱动设备中的并发控制