自旋锁(Spinlock)是一种忙等待锁,线程反复检查锁变量是否可用,不会挂起,避免了进程上下文的调度开销,适合阻塞很短时间的场合。当然也就不适合单CPU单线程上使用。
另外需要注意的是,可能是由于安全问题(具体可以看不再安全的OSSpinlock - ibireme),自旋锁在iOS10之后弃用了,使用 os_unfair_lock 代替。
如何使用
我们先来看下3个关键的API。
OSSpinLockLock(&spinlock) 获取锁,线程一直忙等待。阻塞当前线程执行。
OSSpinLockTry(&spinlock) 尝试获取锁,返回bool。当前线程锁失败,也可以继续其它任务,不阻塞当前线程。
OSSpinLockUnlock(&spinlock) 解锁,参数是OSSpinLock地址。
使用方法比较简单。
//swift
private var spinlock:OSSpinLock?
public func run() {
NSLog("start")
spinlock = OSSpinLock()
//thread1
DispatchQueue.global(qos: .default).async {
NSLog("1-1:"+Thread.current.description);
OSSpinLockLock(&(self.spinlock!))
NSLog("1-2:"+Thread.current.description);
sleep(3)
OSSpinLockUnlock(&(self.spinlock!))
NSLog("1-3:"+Thread.current.description);
}
//thread2
DispatchQueue.global(qos: .default).async {
NSLog("2-1:"+Thread.current.description);
OSSpinLockLock(&(self.spinlock!))
NSLog("2-2:"+Thread.current.description);
sleep(3)
OSSpinLockUnlock(&(self.spinlock!))
NSLog("2-3:"+Thread.current.description);
}
//thread3
DispatchQueue.global(qos: .default).async {
NSLog("3-1:"+Thread.current.description);
if (OSSpinLockTry(&(self.spinlock!))) {
NSLog("3-2:"+Thread.current.description);
sleep(3)
OSSpinLockUnlock(&(self.spinlock!))
}
NSLog("3-3:"+Thread.current.description);
}
NSLog("end")
}
同样需要注意下OSSpinLockLock
和OSSpinLockTry
的区别,和pthread_mutex
一样,trylock
不会阻塞当前线程,获取锁失败,不会休眠,继续执行。
我们来看下输出,发现
3-3
在1-2
和2-2
之前就输出了,也印证了trylock
、lock
两者的区别。
底层实现
自旋锁是如何实现的呢?引用 wikipedia x86下spinlock的汇编实现。
#The following example uses x86 assembly language to implement a spinlock. It will work on any Intel 80386 compatible processor.
; Intel syntax
//初始化锁
locked: ; The lock variable. 1 = locked, 0 = unlocked.
dd 0
//加锁
spin_lock:
mov eax, 1 ; Set the EAX register to 1.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
; This will always store 1 to the lock, leaving
; the previous value in the EAX register.
test eax, eax ; Test EAX with itself. Among other things, this will
; set the processor's Zero Flag if EAX is 0.
; If EAX is 0, then the lock was unlocked and
; we just locked it.
; Otherwise, EAX is 1 and we didn't acquire the lock.
jnz spin_lock ; Jump back to the MOV instruction if the Zero Flag is
; not set; the lock was previously locked, and so
; we need to spin until it becomes unlocked.
ret ; The lock has been acquired, return to the calling
; function.
//解锁
spin_unlock:
mov eax, 0 ; Set the EAX register to 0.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
ret ; The lock has been released.
先介绍一下几个汇编指令:
mov: 数据传输 xchg: 交换数据 test: 两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位 jnz: jump if zero flag not zero,zf不为0的时候跳转
接下来我们看下具体的汇编实现。
locked
初始化锁,默认0。0未锁定,1已锁定,这个比较简单。
spin_lock
加锁操作,主要分4步。
- 把1传输到
eax
; - 交换
locked
、eax
寄存器,也就是把1传输到locked
寄存器中,eax
存储之前的locked
变量; eax
逻辑与运算,本次运算结果存储到zero flag
里。如果eax==0,获得锁,且锁定锁。- 如果zf != 0(eax==1),之前的锁未解锁,当前没有获得锁,跳回 spin_lock MOV指令,也就是忙等待。
spin_unlock
解锁操作,把0传输到locked
寄存器里,也比较简单。
存在的问题
在系统使用基于优先级(priority
)的抢占式调度算法时,high priority
线程始终会在low priority
线程前执行,可能会出现优先级反转的问题。
简单来说,如果一个low priority
线程获得锁并访问共享资源,这时一个high priority
线程也尝试获得这个锁,它会处于spin lock 的忙等状态,占用大量CPU。此时low priority
线程无法与high priority
线程争夺CPU时间,从而导致任务无法正常完成,以及释放锁。
举个🌰:
task1:high priority,要使用资源data
task2:low priority,要使用资源data
spin_lock:一把自旋锁
1.先执行task2
,拿到spin_lock
,由于任务比较耗时,未执行完,继续持有锁。
2.紧接着执行task1
,等待锁释放。由于优先级高,得到系统分配更多的CPU。
3.task1
得不到CPU时间,无法正常执行,因此形成短暂死锁。
os_unfair_lock
为了避免自旋锁的安全问题,在iOS10之后,Apple
不建议使用OSSpinLock
,使用os_unfair_lock
代替。
'OSSpinLock' was deprecated in iOS 10.0: Use os_unfair_lock() from <os/lock.h> instead
我们来看下常用的API:
//加锁,阻塞
os_unfair_lock_lock()
//尝试加锁,不阻塞,成功返回false
os_unfair_lock_trylock()
//解锁
os_unfair_lock_unlock()
使用也很简单。
//swift
public func test_unfair_lock() {
NSLog("start")
unfairlock = .allocate(capacity: 1)
unfairlock?.initialize(to: os_unfair_lock())
for i in 1...2 {
DispatchQueue.global(qos: .default).async {
os_unfair_lock_lock(self.unfairlock!)
sleep(2)
NSLog("\(i):"+Thread.current.description);
os_unfair_lock_unlock(self.unfairlock!)
}
}
//trylock
DispatchQueue.global(qos: .default).async {
if (os_unfair_lock_trylock(self.unfairlock!)) {
sleep(2)
NSLog("3:"+Thread.current.description);
os_unfair_lock_unlock(self.unfairlock!)
}
}
NSLog("end")
}
use mutex?
出于好奇os_unfair_lock
底层实现,翻阅Swift Foundation源码,打开CFInternal.h
文件,找到以下代码:
#if __has_include(<os/lock.h>)
#include <os/lock.h>
#elif _POSIX_THREADS
#define OS_UNFAIR_LOCK_INIT PTHREAD_MUTEX_INITIALIZER
typedef pthread_mutex_t os_unfair_lock;
typedef pthread_mutex_t * os_unfair_lock_t;
CF_INLINE void os_unfair_lock_lock(os_unfair_lock_t lock) { pthread_mutex_lock(lock); }
CF_INLINE void os_unfair_lock_unlock(os_unfair_lock_t lock) { pthread_mutex_unlock(lock); }
#elif defined(_WIN32)
...
我们发现,os_unfair_lock
并没有使用pthread_mutex
,优先使用<os/lock.h>
文件里的定义。不过这个文件只有定义,看不到具体实现。
//<os/lock.h>
void os_unfair_lock_lock(os_unfair_lock_t lock);
bool os_unfair_lock_trylock(os_unfair_lock_t lock);
......
当然我们也可以通过Symbol Breakpoint
调用堆栈来判断,发现os_unfair_lock_lock
、pthread_mutex_lock
两者之间没有调用关系。
添加
pthread_mutex_lock
的Symbol Breakpoint
,在os_unfair_lock_lock
调用之后,启用这个断点,看看调用堆栈。
参考链接
Swift Foundation Spinlock - wikipedia 不再安全的OSSpinlock - ibireme