最近在整理iOS
锁相关的知识,翻阅了网上很多iOS
锁的文章,基本都是起源于ibireme
的不再安全的OSSpinlock。关于锁,多多少少会有下面这些疑问:
- 锁是什么?为什么要有锁?
- 有哪些锁?可以分成哪几种?
- 锁的性能表现如何?
- 这些锁之间有什么关系?
- 为什么 OSSpinLock 不安全?如何解决?
- 锁之间的关系?
- 锁的具体使用方式是怎样的?
接下来我们一个个来解释。
锁是什么
锁是保证线程安全的同步工具,每一个线程在访问数据前,要先获取acquire
锁,访问结束之后释放release
锁。如果锁已经被占用,其它试图获取锁的线程会等待或休眠,直到锁重新可用。
为什么要有锁呢?在多线程编程场景中,多个线程同时访问同一个共享数据,可能会出现数据竞争data race
,容易引发数据错乱等问题。这时候就需要用一种同步机制来保证数据安全,锁就是最常见的同步工具。
有哪些锁
在iOS开发中,常用的有11种锁,当然还有其他可以达到同步效果的API,比如说serial queue
串行队列等,我们整理了一张图,如下:
针对锁的不同特性,我们可以得到不同的分类,按照锁的等待行为,大致可以分为互斥锁和自旋锁。
互斥锁
在尝试加锁的时候,如果锁是加锁状态,还未释放,线程会进入休眠状态等待锁释放。锁释放后,等待资源的线程会被唤醒执行。POSIX
标准接口提供了pthread_mutex
互斥锁。
自旋锁
在尝试加锁的时候,如果锁是加锁状态,还未释放,线程会以循环的方式等待锁释放,锁释放后,等待资源的线程会立即执行。POSIX
标准接口提供了pthread_spin
自旋锁。
不过翻阅了opensource objc4和swift 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
读写锁。
-
pthread_mutex
pthread_mutex
是POSIX
标准接口的互斥锁,OSSpinLock
、NSLock
、NSCondition
、NSConditionLock
都是在这基础之上封装的。 -
pthread_mutex(recursive)
pthread_mutex
递归版本,设置pthread_mutexattr_t
互斥锁属性类型为PTHREAD_MUTEX_RECURSIVE
,synchronized(objc_sync)
、NSRecursiveLock
就是在这个基础上封装的。 -
pthread_cond 条件变量,利用线程间共享的全局变量进行同步的一种机制,为了安全,条件变量总是和一个互斥锁一起使用。
NSCondition
、NSConditionLock
也是在这个基础上封装的。 -
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_trylock
和pthread_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_rwlock
是POSIX
标准接口提供读写锁,支持多读一写,但是读写互斥的,即读的时候不能写,写的时候不能读。常用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")
}
复制代码