iOS OSSpinLock

1,438 阅读5分钟

自旋锁(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")
}

同样需要注意下OSSpinLockLockOSSpinLockTry的区别,和pthread_mutex一样,trylock不会阻塞当前线程,获取锁失败,不会休眠,继续执行。 我们来看下输出,发现3-31-22-2之前就输出了,也印证了trylocklock两者的区别。

底层实现

自旋锁是如何实现的呢?引用 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. 把1传输到eax;
  2. 交换lockedeax寄存器,也就是把1传输到locked寄存器中,eax存储之前的locked变量;
  3. eax逻辑与运算,本次运算结果存储到zero flag里。如果eax==0,获得锁,且锁定锁。
  4. 如果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_lockpthread_mutex_lock两者之间没有调用关系。

添加pthread_mutex_lockSymbol Breakpoint,在os_unfair_lock_lock调用之后,启用这个断点,看看调用堆栈。

参考链接

Swift Foundation Spinlock - wikipedia 不再安全的OSSpinlock - ibireme