自旋锁与互斥锁

本文深入探讨自旋锁与互斥锁的区别,提供实践策略及解决方案,指导如何在并发编程中合理选择锁类型,以优化性能。

自旋锁(spinlock)与互斥锁(mutex)是并发编程中两个重要的概念。它们的主要作用是:对共享资源加锁以阻止数据的并发访问,从而保证数据一致性。但是它们也有一些不同点。本文主要介绍这些不同点,并说明我们什么时候该用自旋锁,什么时候该用互斥锁。


理论基础

理论上,当一个线程尝试去获取一个互斥锁,但由于该互斥锁已经被其它线程获取而没有成功时,它会立刻进入休眠状态,从而让出CPU时间,允许其它线程运行。它将持续休眠直到最终被唤醒,唤醒的条件是之前获取到该互斥锁的线程释放了互斥锁;

对比一下,当一个线程尝试去获取一个自旋锁,但由于该自旋锁已经被其它线程获取而没有成功时, 它将会反复获取它(在英文中叫polling),直到最终成功。因此在获取自旋锁的时候,该线程不会让出CPU时间,其它线程将不能运行,当然,操作系统不会允许一个线程一直阻塞整个系统的运行,在某个线程花完了它的CPU运行总时间后,它会强制切换到另外线程执行。(注:线程一次使用的CPU总时间的最大上限可以通过ulimit -t查看,单位为秒)


问题

对于互斥锁,使线程进入休眠以及唤醒线程都是比较昂贵的操作,需要相当多的CPU指令,花费较长的CPU时间。如果A线程获取到该互斥锁后,只是持有了很短的一段时间就释放,那么B线程在获取互斥锁的过程中,B线程进入休眠以及被唤醒花费的CPU时间可能超过了B线程休眠的时间,甚至可能超过了采用的自旋锁时polling的时间;

另一方面,对于自旋锁,反复获取自旋锁将会花费很多CPU时间,如果A线程持有该锁的时间过长,那么尝试获取该锁的B线程将会占用很多CPU时间,在这种情况下,让B线程进入休眠反而会更好些。


解决方案

不要在单CPU单核的系统上使用自旋锁,因为当A线程获取自旋锁polling的时候,可能会阻塞仅有的CPU导致其它所有的线程都不能运行,而其它线程不能运行意味着需要释放该自旋锁的B线程也得不到CPU时间不能运行,那么该自旋锁将不会被释放。也就是说,自旋锁浪费了仅有的系统CPU时间而没有获得任何好处(能获取到锁的条件是polling耗费掉其CPU总时间后,系统切换到B线程,B线程释放锁后,A线程再次执行时才获得锁)。如果A线程在不能获取到锁的时候就立刻进入休眠,B线程可能就可以立刻执行,有可能B线程会立刻释放掉锁,在A线程被唤醒后,就可以继续执行。

在多CPU多核的系统上,如果大量的锁被持有的时间都很短,那么浪费在不断使线程进入休眠和唤醒线程的时间将会很多,有可能显著的降低系统性能。当采用自旋锁的时候,线程可以有机会利用它们可以使用的CPU时间片,在短暂的阻塞后立刻进行后续的工作,从而最大限度的提高CPU的使用率。


实践策略

因为在多数情况下程序员不知道到底是使用自旋锁还是互斥锁更好(因为不能提前知道程序运行的系统CPU核数),操作系统也不可能知道某段代码被优化为运行在单核或多核环境下,大多数操作系统也并不能严格区分互斥锁还是自旋锁。事实上,多数现代操作系统均采用混合的互斥锁和混合的自旋锁,这是为什么呢?

混合的互斥锁的行为在开始时类似于在多核系统上的自旋锁:如果A线程不能获取锁,它不会被立刻休眠,因为该互斥锁可能会立刻被释放,所以互斥锁开始时表现会类似于自旋锁。只有在A线程等待了一段足够长的时间后还没有获得该互斥锁时才会被休眠。如果同样的程序运行在一个单核系统上,那么互斥锁将不会自旋,因为这没有任何好处。

混合的自旋锁的行为在开始时和一般的自旋锁一样,但是为了避免浪费过多的CPU时间,它可能会采用一个折中的策略:系统不会让线程进入休眠(因为使用自旋锁时你肯定不希望休眠发生),但是会决定什么时候停止线程让其他线程运行(要么立刻,要么在一段固定时间后),因此增加了自旋锁解锁的机率。(单纯的线程切换通常没有使线程进入休眠再唤醒线程代价那么昂贵)


总结

如果不确定,那么使用互斥锁,这通常是更好的选择。在能够获得益处的情形下,大多数的现代操作系统将允许互斥锁自旋(polling)一段时间。采用自旋锁在某些时候会提高性能,但是只是在某些特定的条件下。事实上,你是在怀疑而不是告诉我,你目前没有任何项目采用自旋锁可能获益。你可能会采用你自己定义的“锁对象”,它在内部要么使用自旋锁,要么使用互斥锁(例如,使用何种类型的锁在创建时是可配置的),初始时,在所有的地方都使用互斥锁,如果在某些地方使用自旋锁可能会有帮助,那么请试一试,并比较二者的结果,在得出结论前,一定要确保比较的时候要测试了单核和多核系统,如果你的程序是跨平台的,也请测试各种操作系统。


原文:http://www.pixelstech.net/article/1397962421-Practice-of-using-spinlock-instead-of-mutex

### 自旋锁互斥锁的区别 #### 等待机制 自旋锁通过忙等待(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(); ``` ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值