自旋锁与互斥锁的使用场景分析

本文对比分析自旋锁与互斥锁的区别及使用场景。自旋锁适用于短期临界区代码,中断处理中优先选用。互斥锁则用于需要睡眠的临界区。文章还探讨了不同自旋锁变种的适用场景及其性能影响。
本文不对自旋锁和互斥锁的概念做阐述,重点分析它们之间的区别和自旋锁的使用场景。

自旋锁和互斥锁的区别

a. 互斥锁加锁失败后,线程会释放 CPU,给其他线程;自旋锁加锁失败后,线程会忙等待,直到它拿到锁;
b. 自旋锁会关闭本CPU内核抢占,互斥锁不会.
问:为什么自旋锁要关闭本CPU内核抢占?
答:加锁进程A获取一把自旋锁之后,开始执行临界区代码,此时发生了调度,执行进程B。恰巧进程B需要用到该自旋锁,这就导致了死锁的发生.

自旋锁和互斥锁的使用场景

a. 如果我们明确知道被锁住的代码的执行时间很短,那我们应该选择开销比较小的自旋锁
b. 如果临界区需要睡眠,应该选择互斥锁
c. 中断里应该使用自旋锁,因为中断处理中不允许睡眠

自旋锁在中断场景使用需要注意的点

a. spin_lock_irq的使用
问1:中断的到来,一定会打断正在执行的 CPU 的进程,哪怕是当前的程序被 spin_lock 给锁住了,假设当前的中断也需要访问该 spin_lock 锁,那么就会导致死锁发生。
解决:所以应该在进程执行临界区代码段的自旋锁上锁之前,首先就应该关闭当前 CPU 的中断,这样无论你发生什么情况,一旦被锁上就不再会被任何的情况抢走 CPU,这就是 spin_lock_irq 出现的原因.
缺点:由于需要关闭当前CPU中断,会导致系统的响应降低,性能下降.

b. spin_lock_irqsave的使用
问2:自旋锁嵌套使用
在这里插入图片描述
死锁原因:当进程在执行第一个 spin_lock_irq(&lock1) 时已经关闭了中断,然后继续获取第二个自旋锁 spin_lock_irq(&lock2),当 lock2 解锁后且 lock1 未解锁前这一段区域处于一种硬件中断开启的状态,
这段代码仍然处于 lock1 的临界区,如果此时硬件中断也需要申请 lock1 锁会导致死锁,这是因为 spin_unlock_irq 在解锁时一定会将本地 CPU 的中断打开,从而导致硬件中断可以重新强制抢占 CPU.
解决:所以最好的方式就是 lock2 在锁之前保存当前的中断状态,在解锁时恢复当前的中断状态,这也就是 spin_lock_irqsave 需要存在的原因。自然地,这是一种更为线程安全的方案,但是他会带来比前面几种更加多的性能损耗。

总结

spin_lock 使用场景

首先如果整个临界区都只位于进程上下文或者工作队列中,那么只需要采用最为方便的 spin_lock 即可,因为他不会发生中断抢占锁的情况,哪怕中断抢占进程上下文也不会导致中断由于申请自旋锁而导致死锁。还有一种情况就是在硬件中断中可以考虑使用 spin_lock 即可,因为硬件中断不存在嵌套(未必一定是这样,与平台有关),所以只需要简单的上锁即可, 可以不需要关闭中断,保存堆栈等。

spin_lock_irq 使用场景

这个锁的变种适合在进程上下文/软中断 + 硬件中断这样的组合中使用,taskset 也是属于软中断的一种,所以也归在此类。当然,这种类型的变种同样适合软中断/taskset + 进程上下文的组合,因为关闭了硬件中断,从源头就禁止执行软中断代码,不过,对于这种类型的中断最好的方式是使用 spin_lock_bh 的方式,因为他只锁定软中断代码执行,而不关闭硬件中断,这样性能损耗更小。

spin_lock_irqsave 使用场景

这种类型的使用方式是最为安全以及便捷的,毕竟不需要考虑会不会发生死锁的问题(代码本身引入的死锁不在此类),但是他也是性能损耗最大的代码,能不使用尽量不适用,在高速设备上,自旋锁已然成为了一种降低性能的瓶颈。他最好只出现在在需要尝试 spin_lock 之前无法确定是否已经关闭中断的代码才使用,如果代码能够确定在执行锁之前中断一定是打开的,那么使用 spin_lock_irq 是更佳的选择。

spin_lock_bh 使用场景

这种类型的变种是一种比 spin_lock_irq 更轻量的变种,只关闭中断底半部,其实就是关闭了软中断、Taskset 以及 Timer 等的一个抢占能力,如果开发者确定编写的代码临界区只存在软中断/Taskset/Timer + 进程上下文这样的组合,则最好考虑使用 spin_lock_bh 这样的锁来禁止软中断进行抢占。还有就是软中断与软中断自我抢占临界区访问时,也需要使用 spin_lock_bh 以上的中断锁,因为有可能软中断在执行的过程中,自己被硬件中断打断,然后又执行到同样的代码,在别的 CPU 执行还好说,毕竟软中断可以在不同的 CPU 上执行同一个中断函数,但是假设不幸运行在同一个 CPU 上,则会导致死锁。Taskset 由于在运行过程中钟只会运行一个实例,所以不存在死锁问题,Taskset 与 Taskset 的锁竞争只需要使用 spin_lock 即可。

### 自旋锁互斥锁的区别 #### 等待机制 自旋锁通过忙等待(Busy-Wait)持续检查锁的状态,线程在等待期间保持运行状态,不释放CPU资源。互斥锁则在等待锁时主动让出CPU,进入阻塞状态,由操作系统调度唤醒[^3]。 ```c static ALWAYS_INLINE void OSSpinLockLock(volatile OSSpinLock *lock) { do { while (lock->value != 0) { // 忙等待 - 线程一直在CPU上运行 __asm__ volatile ("pause"); } } while (!OSAtomicCompareAndSwap32(0, 1, &lock->value)); } ``` #### 实现原理 自旋锁依赖原子操作(如CAS、Test-And-Set)实现,通常在用户态完成,无需内核介入。互斥锁则依赖操作系统提供的阻塞/唤醒机制(如信号量、条件变量),涉及内核态切换。 #### 性能特点 自旋锁的优点是无上下文切换开销,适合锁持有时间极短的场景(如几纳秒),缺点是长时间等待会浪费CPU资源。互斥锁的优点是等待时不占用CPU,适合锁持有时间较长或不可预测的场景,缺点是上下文切换可能带来较大开销[^3]。 #### 适用场景 自旋锁适用于多核系统,尤其是临界区代码极短(如内核中断处理)、线程不允许休眠的场景(如某些实时系统)以及用户态高性能同步(需结合自适应策略)。互斥锁适用于用户态应用程序,尤其是临界区代码较复杂或耗时较长(如文件操作)、单核CPU环境以及需要避免CPU资源浪费的场景[^3]。 #### 其他差异 自旋锁可能导致优先级反转(高优先级线程空转等待低优先级线程),而互斥锁通常支持优先级继承等机制解决优先级反转问题。此外,自旋锁在单核系统中需禁用中断或配合调度策略,否则可能死锁,而互斥锁适用于所有CPU架构。 #### 性能对比 自旋锁无需保存/恢复上下文,无需切换线程,响应更快,但占用CPU资源。互斥锁需要保存/恢复上下文,需要切换线程,响应较慢,但不占用CPU资源[^5]。 #### 使用场景示例 自旋锁适合短期、快速的操作,如计数器操作: ```c spinlock.lock(); counter++; // 非常快的操作 spinlock.unlock(); ``` 互斥锁适合长时间操作或可能阻塞的操作: ```c mutex.lock(); // 长时间操作或可能阻塞的操作 [self complexOperation]; mutex.unlock(); ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值