老青菜

iOS中常用的锁

2020-03-25

最近在整理iOS锁相关的知识,翻阅了网上很多iOS锁的文章,基本都是起源于ibireme不再安全的OSSpinlock。关于锁,多多少少会有下面这些疑问:

  1. 锁是什么?为什么要有锁?
  2. 有哪些锁?可以分成哪几种?
  3. 锁的性能表现如何?
  4. 这些锁之间有什么关系?
  5. 为什么 OSSpinLock 不安全?如何解决?
  6. 锁之间的关系?
  7. 锁的具体使用方式是怎样的?

接下来我们一个个来解释。

锁是什么

锁是保证线程安全的同步工具,每一个线程在访问数据前,要先获取acquire锁,访问结束之后释放release锁。如果锁已经被占用,其它试图获取锁的线程会等待或休眠,直到锁重新可用。

为什么要有锁呢?在多线程编程场景中,多个线程同时访问同一个共享数据,可能会出现数据竞争data race,容易引发数据错乱等问题。这时候就需要用一种同步机制来保证数据安全,锁就是最常见的同步工具。

有哪些锁

在iOS开发中,常用的有11种锁,当然还有其他可以达到同步效果的API,比如说serial queue串行队列(
性能最差
)等,我们整理了一张图,如下:

针对锁的不同特性,我们可以得到不同的分类,按照锁的等待行为,大致可以分为互斥锁和自旋锁。

互斥锁

在尝试加锁的时候,如果锁是加锁状态,还未释放,线程会进入休眠状态等待锁释放。锁释放后,等待资源的线程会被唤醒执行。POSIX标准接口提供了pthread_mutex互斥锁。

自旋锁

在尝试加锁的时候,如果锁是加锁状态,还未释放,线程会以循环的方式等待锁释放,锁释放后,等待资源的线程会立即执行。POSIX标准接口提供了pthread_spin自旋锁。

不过翻阅了opensource objc4swift foundation源码都没有找到pthread_spin的API,可能是Apple没有暴露pthread_spin相关接口。

性能表现

接下来,我们做一个性能测试,看下各种锁的表现如何,完整代码都在这里

//swift
///循环10w次,重复加锁解锁操作
public func testLockPerformance() {
    let looppCount:Int = 100000
    var begin:TimeInterval = 0
    var end:TimeInterval = 0
    //spinlock
    begin = CFAbsoluteTimeGetCurrent()
    spinlock = OSSpinLock()
    for _ in 1...looppCount {
        OSSpinLockLock(&(self.spinlock!))
        OSSpinLockUnlock(&(self.spinlock!))
    }
    end = CFAbsoluteTimeGetCurrent()
    NSLog("spinlock:\((end-begin)*1000)")
    //由于代码太多,这里省略,上面有完整代码的链接。
}

输出日志:


转换成更直观的条形图:

经过多次尝试,输出结果大致相似,pthread_mutex最快,其次是pthread_rw_lock,然后是os_unfair_lock

锁之间关系

这些锁之间的关系是怎么样的呢?通过翻阅这些锁的实现,发现iOS大部分锁都是基于pthread_mutex的封装,顺便整理出一张关系图,如下:

最下面是POSIX可移植的操作系统接口。提供mutex互斥锁、recursive递归锁、cond条件变量、rw读写锁。

  1. pthread_mutex
    pthread_mutexPOSIX标准接口的互斥锁,OSSpinLockNSLockNSConditionNSConditionLock都是在这基础之上封装的。

  2. pthread_mutex(recursive)
    pthread_mutex递归版本,设置pthread_mutexattr_t互斥锁属性类型为PTHREAD_MUTEX_RECURSIVEsynchronized(objc_sync)NSRecursiveLock就是在这个基础上封装的。

  3. pthread_cond
    条件变量,利用线程间共享的全局变量进行同步的一种机制,为了安全,条件变量总是和一个互斥锁一起使用。NSConditionNSConditionLock也是在这个基础上封装的。

  4. pthread_rwlock
    读写锁,可以实现多读一写的效果,但是读和写不能同时进行。

接下来我们详细分析每个锁的使用。

pthread_mutex

POSIX标准接口的互斥锁,常用API:

//互斥锁属性,支持递归
pthread_mutexattr_init(&attr) 可选,初始化互斥锁属性
pthread_mutexattr_settype(&attr) 可选,设置互斥锁属性类型,PTHREAD_MUTEX_RECURSIVE:递归
//互斥锁
pthread_mutex_init(&mutex,&attr) 初始化互斥锁
pthread_mutex_lock(&mutex) 加锁,阻塞
pthread_mutex_trylock(&mutex) 尝试加锁,不阻塞,成功返回0
pthread_mutex_unlock(&mutex) 解锁
pthread_mutex_destroy(&mutex) 销毁锁

使用比较简单。

//swift
public func test_pthread_mutex() {
    NSLog("start")
    mutex = pthread_mutex_t()
    pthread_mutex_init(&(self.mutex!), nil)
    for i in 1...2 {
        DispatchQueue.global(qos: .default).async {
            pthread_mutex_lock(&(self.mutex!))
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))
        }
    }
    //trylock
    DispatchQueue.global(qos: .default).async {
        let retCode = pthread_mutex_trylock(&(self.mutex!))
        if( retCode == 0) {
            sleep(2)
            NSLog("3:"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))
        }
        NSLog("3-1:"+Thread.current.description);
    }
    NSLog("end")
}

这里需要注意下,pthread_mutex_trylockpthread_mutex_lock的区别,前者不会阻塞当前线程,如果获取锁失败,函数返回不为0。且继续往下执行;而后者会阻塞当前线程,获取锁失败,当前线程会休眠。

pthread_mutex recursive

pthread_mutex支持递归锁,在初始化pthread_mutex时候,设置pthread_mutexattr_t互斥属性的类型为PTHREAD_MUTEX_RECURSIVE递归类型。

//swift
public func test_pthread_mutex_recursive() {
    NSLog("start")
    mutex = pthread_mutex_t()
    var attr = pthread_mutexattr_t()
    pthread_mutexattr_init(&attr)
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)
    pthread_mutex_init(&(self.mutex!), &attr)
    for i in 1...2 {
        DispatchQueue.global(qos: .default).async {
            pthread_mutex_lock(&(self.mutex!))
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
            pthread_mutex_unlock(&(self.mutex!))
        }
    }
    NSLog("end")
}

pthread_rwlock

pthread_rwlockPOSIX标准接口提供读写锁,支持多读一写,但是读写互斥的,即读的时候不能写,写的时候不能读。常用API如下:

///销毁
public func pthread_rwlock_destroy(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
///初始化
public func pthread_rwlock_init(_: UnsafeMutablePointer<pthread_rwlock_t>
, _: UnsafePointer<pthread_rwlockattr_t>?) -> Int32
///获取读锁
public func pthread_rwlock_rdlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
///尝试获取读锁
public func pthread_rwlock_tryrdlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
///尝试获取写锁
public func pthread_rwlock_trywrlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
///获取写锁
public func pthread_rwlock_wrlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32
///解锁
public func pthread_rwlock_unlock(_: UnsafeMutablePointer<pthread_rwlock_t>) -> Int32

使用起来也很简单。

//swift 
public func test_pthread_rwlock() {
    NSLog("start")
    self.rwlock = pthread_rwlock_t()
    //初始化读写锁
    pthread_rwlock_init(&(self.rwlock!), nil)
    DispatchQueue.global(qos: .default).async {
         //读
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read1:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))
    }
    DispatchQueue.global(qos: .default).async {
         //读
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read2:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))
    }
    DispatchQueue.global(qos: .default).async {
         //写
        pthread_rwlock_wrlock(&(self.rwlock!))
        sleep(3)
        NSLog("write1:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))
    }
    DispatchQueue.global(qos: .default).async {
         //写
        pthread_rwlock_wrlock(&(self.rwlock!))
        sleep(3)
        NSLog("write2:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))
    }
    DispatchQueue.global(qos: .default).async {
         //读
        pthread_rwlock_rdlock(&(self.rwlock!))
        sleep(3)
        NSLog("read3:"+Thread.current.description)
        pthread_rwlock_unlock(&(self.rwlock!))
    }    
    NSLog("end")
}

semaphore

DispatchSemaphore信号量是持有计数的信号,通过计数来控制多线程对资源的访问。
关于信号量有个很容易理解的🌰。信号量类似停车场空位,车辆出去发送signal,空位+1,车辆进入发送wait,空位-1,当空位<=0,车辆不能进入,wait会阻塞。
我们来看看常用的API:

///发送信号,信号量+1
public func signal() -> Int
///等待信号,信号量-1
public func wait()
///等待信号,设置最晚等待时间
public func wait(timeout: DispatchTime) -> DispatchTimeoutResult
public func wait(wallTimeout: DispatchWallTime) -> DispatchTimeoutResult

使用起来也很简单:

//swift
public func test_semaphore() {
    NSLog("start")
    //初始化,初始信号2,同时允许两个thread访问
    semaphore = DispatchSemaphore(value: 2)
    for i in 1...4 {
        DispatchQueue.global(qos: .default).async {
              //等待信号
            self.semaphore?.wait()
            sleep(2)
            NSLog("\(i):"+Thread.current.description);
              //释放信号
            self.semaphore?.signal()
        }
    }
    NSLog("end")
}

serial queue

这里稍微提一下serial queue串行队列,我们把临界区的访问放在串行队列里,也可以达到同步的效果。我们来看看使用方法:

//swift
///声明serialQueue lazy
private lazy var serialQueue:DispatchQueue = {
        return DispatchQueue(label: "queue1", qos: .default, attributes: DispatchQueue.Attributes.init(rawValue: 0), autoreleaseFrequency: .inherit, target: nil)
    }()


public func test_serial_queue() {
    NSLog("start")
    for i in 1...4 {
        DispatchQueue.global(qos: .default).async {
              //临界区访问放在串行队列里
            self.serialQueue.async {
                sleep(2)
                NSLog("\(i):"+Thread.current.description);
            }
        }
    }
    NSLog("end")
}

The Next

  1. iOS 中常用的锁
  2. iOS OSSpinLock
  3. iOS Synchronized
  4. iOS NSLock 底层分析
  5. iOS Atomic 底层分析

参考链接

swift foundation
opensource objc4

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章