自旋锁和信号量主要有四个不同点:
1. 它们是什么
A spinlock是锁的一种可能实现,即通过忙等待(“旋转”)实现的锁。信号量是锁的概括(或者,相反,锁是信号量的特例)。通常,但不一定,自旋锁仅在一个进程内有效,而信号量也可用于在不同进程之间进行同步。
锁的作用是互斥,即one线程一次可以获得锁并继续执行代码的“关键部分”。通常,这意味着修改多个线程共享的某些数据的代码。
A 信号有一个计数器,并允许自己被一个或几个线程,取决于您向其发布的值,以及(在某些实现中)取决于其最大允许值。
到目前为止,我们可以将锁视为最大值为 1 的信号量的一种特殊情况。
2. 他们做什么
如上所述,自旋锁是一种锁,因此是一种互斥(严格1对1)机制。它的工作原理是通常以原子方式重复查询和/或修改内存位置。这意味着获取自旋锁是一项“繁忙”操作,可能会长时间(也许永远!)消耗 CPU 周期,而实际上却“一无所获”。
这种方法的主要动机是上下文切换的开销相当于旋转几百(甚至几千)次,因此如果可以通过燃烧几个周期旋转来获取锁,那么总体上来说这很可能是更高效。此外,对于实时应用程序来说,阻塞并等待调度程序在未来某个遥远的时间返回它们可能是不可接受的。
相比之下,信号量要么根本不旋转,要么只旋转很短的时间(作为避免系统调用开销的优化)。如果无法获取信号量,它将阻塞,将 CPU 时间让给准备运行的其他线程。当然,这可能意味着在再次调度线程之前需要几毫秒的时间,但如果这没有问题(通常不是),那么它可能是一种非常高效、CPU 保守的方法。
3. 他们在拥堵时的表现
一种常见的误解是,自旋锁或无锁算法“通常更快”,或者它们仅对“非常短的任务”有用(理想情况下,同步对象的持有时间不应超过绝对必要的时间)。
一个重要的区别是不同方法的行为方式出现拥堵时.
设计良好的系统通常拥塞程度较低或没有拥塞(这意味着并非所有线程都尝试同时获取锁)。例如,人们通常会not编写获取锁的代码,然后从网络加载半兆字节的 zip 压缩数据,解码和解析数据,最后在释放锁之前修改共享引用(将数据附加到容器等)。相反,人们获取锁只是为了访问共享资源.
由于这意味着临界区外部的工作比临界区内部的工作多得多,因此线程位于临界区内部的可能性自然相对较低,因此很少有线程同时竞争锁。当然,时不时两个线程会尝试同时获取锁(如果这couldn't碰巧你不需要锁!),但这只是一个例外,而不是“健康”系统中的规则。
在这种情况下,自旋锁greatly优于信号量,因为如果没有锁拥塞,获取自旋锁的开销仅为几十个周期,而上下文切换需要数百/数千个周期,或者丢失时间片剩余部分需要 10-2000 万个周期。
另一方面,如果拥塞程度很高,或者锁被持有很长时间(有时你就是无能为力!),自旋锁会消耗大量的 CPU 周期而一事无成。
在这种情况下,信号量(或互斥量)是更好的选择,因为它允许运行不同的线程useful那段时间的任务。或者,如果没有其他线程有有用的事情可做,它允许操作系统降低 CPU 速度并减少热量/节省能源。
此外,在单核系统上,自旋锁在存在锁拥塞的情况下效率非常低,因为自旋线程将浪费其全部时间来等待不可能发生的状态更改(直到释放线程被调度为止,这没有发生当等待线程正在运行时!)。因此,给定any由于争用量较大,在最好的情况下获取锁大约需要 1 1/2 时间片(假设释放线程是下一个被调度的线程),这不是很好的行为。
4. 如何实施
如今,信号量通常会换行sys_futex
在 Linux 下(可选地使用在几次尝试后退出的自旋锁)。
自旋锁通常使用原子操作来实现,而不使用操作系统提供的任何内容。在过去,这意味着使用编译器内部函数或不可移植的汇编器指令。同时,C++11 和 C11 都将原子操作作为语言的一部分,因此除了编写可证明正确的无锁代码的一般困难之外,现在可以以完全可移植且(几乎)的方式实现无锁代码。无痛方式。