在高性能锁的设计中,Adaptive Spinning(自旋自适应) 是一种混合策略,旨在平衡“空转等待”的低延迟和“挂起线程”的低功耗。
简单来说,它让 pthread_mutex 变得“聪明”了:它不再是上来就直接让线程睡觉,而是先观察一下,看能不能通过几微秒的自旋直接拿到锁。
1. 为什么需要自适应?
在多核处理器中,线程同步通常面临两种选择:
- 自旋(Spinning): CPU 循环执行
NOP或忙等。优点是响应极快,没有上下文切换(Context Switch)开销;缺点是极其耗电,且如果锁持有时间长,会浪费 CPU 周期。 - 阻塞(Blocking): 线程进入休眠,交出 CPU。优点是不占资源;缺点是唤醒时需要内核参与,上下文切换大约需要几微秒(对于纳秒级的操作来说太慢了)。
Adaptive Spinning 的逻辑: 如果锁的持有者正在另一个 CPU 核心上运行,那么它很有可能马上就会释放锁。此时自旋一小会儿是划算的。
2. 工作原理:观察与反馈
当线程尝试获取一个 PTHREAD_MUTEX_ADAPTIVE_NP(非标准扩展)类型的锁时,它会经历以下阶段:
-
观察环境: 检查锁的持有者是否正在运行。如果持有者已经挂起(Sleeping),当前线程会直接选择放弃自旋进入阻塞,因为持有者醒来并释放锁的时间一定很长。
-
动态自旋: 如果持有者正在运行,当前线程会开始自旋。自旋的次数不是固定的,而是根据历史成功率动态调整。
- 如果最近几次自旋都很快拿到了锁,系统会增加下次自旋的次数上限。
- 如果最近自旋总是失败最后还是得靠阻塞解决,系统会减少自旋次数,甚至直接跳过自旋。
-
最终降级: 如果达到自旋上限仍未获得锁,线程才会执行
syscall进入内核态挂起。
3. iOS 中的情况:os_unfair_lock 的异曲同工
虽然 pthread_mutex 在 Linux 等系统上广泛支持自适应模式,但在 iOS/macOS 平台上,苹果更倾向于推行 os_unfair_lock。
os_unfair_lock 实际上也包含了类似的逻辑:
- 它在用户态进行极短时间的自旋。
- 它解决了
OSSpinLock的优先级反转问题:如果高优先级线程在自旋等待低优先级线程,内核会感知到这种竞争,并将低优先级线程的优先级临时提升,让其尽快完成任务并释放锁。
4. 性能对比
| 策略 | 场景 | CPU 占用 | 响应速度 |
|---|---|---|---|
| 纯自旋 (Spinlock) | 极短的任务 | 极高 | 最快 |
| 纯互斥 (Mutex) | 长时间的任务 | 极低 | 慢(受限于上下文切换) |
| 自适应 (Adaptive) | 绝大多数场景 | 中等 | 接近自旋,安全性接近互斥 |
5. 什么时候该手动开启?
在 POSIX 接口中,你需要显式设置属性:
C
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
// PTHREAD_MUTEX_ADAPTIVE_NP 在某些 Unix 系统中可用
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_ADAPTIVE_NP);
pthread_mutex_init(&lock, &attr);
注意: 在 iOS 开发中,NSLock 默认并不开启自适应自旋,它主要基于底层的 pthread_mutex_lock 默认实现。如果你在写极高性能的底层 C++ 库,手动配置自适应属性会有显著提升。
总结
Adaptive Spinning 是锁的一种**“博弈策略”**。它利用历史经验预测未来,从而在“浪费 CPU 周期”和“浪费上下文切换时间”之间找到最优平衡点。