iOS 线程安全和锁机制
一、线程安全场景
多个线程中同时访问同一块资源,也就是资源共享。多牵扯到对同一块数据的读写操作,可能引发数据错乱问题。
比较经典的线程安全问题有购票和存钱取钱问题,为了说明读写操作引发的数据混乱问题,以存钱取钱问题来做个说明。
1. 购票案例
用代码示例如下:
@IBAction func ticketSale() {
tickets = 30
let queue = DispatchQueue.global()
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
}
//卖票
func sellTicket() {
var oldTicket = tickets
sleep(UInt32(0.2))
oldTicket -= 1
tickets = oldTicket
print("还剩\(tickets)张票 ---- \(Thread.current)")
}
同时有三条线程进行卖票,对余票进行减操作,三条线程每条卖10次,原定票数30,应该剩余票数为0,下面看下打印结果:
可以看到打印票数不为0
2. 存钱取钱案例
先用个图说明
上图可以看出,存钱和取钱之后的余额理论上应该是500,但由于存钱取钱同时访问并修改了余额,导致数据错乱,最终余额可能变成了400,下面用代码做一下验证说明:
//存钱取钱
@IBAction func remainTest() {
remain = 500
let queue = DispatchQueue.global()
queue.async {
for _ in 0..<5 {
self.saveMoney()
}
}
queue.async {
for _ in 0..<5 {
self.drawMoney()
}
}
}
//存钱
func saveMoney() {
var oldRemain = remain
sleep(2)
oldRemain += 100
remain = oldRemain
print("存款100元,账户余额还剩\(remain)元 ----\(Thread.current)")
}
//取钱
func drawMoney() {
var oldRemain = remain
sleep(2)
oldRemain -= 50
remain = oldRemain
print("取款50元,账户余额还剩\(remain)元 ---- \(Thread.current)")
}
上述代码存款5次100,取款5次50,最终的余额应该是 500 + 5 * 100 - 5 * 50 = 750
如图所示,可以看到在存款取款之间已经出现错乱了
上述两个案例之所以出现数据错乱问题,就是因为有多个线程同时操作了同一资源,导致数据不安全而出现的。
那么遇到这个问题该怎么解决呢?自然而然的,我们想到了对资源进行加锁处理,以此来保证线程安全,在同一时间,只允许一条线程访问资源。
加锁的方式大概有以下几种:
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
1. OSSpinLock 自旋锁
OSSpinLock
是自旋锁,在系统框架 libkern/OSAtomic
下
如图,系统提供了以下几个API
- 定义lock
let osspinlock = OSSpinLock()
OSSpinLockTry
官方给定的解释如下
Locks a spinlock if it would not block
return false, if the lock was already held by another thread,
return true, if it took the lock successfully.
尝试加锁,加锁成功则继续,加锁失败则直接返回,不会阻塞线程
OSSpinLockLock
Although the lock operation spins, it employs various strategies to back
off if the lock is held.
加锁成功则继续,加锁失败,则会阻塞线程,处于忙等状态
OSSpinLockUnlock
: 解锁
使用
@IBAction func ticketSale() {
osspinlock = OSSpinLock()
tickets = 30
let queue = DispatchQueue.global()
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
queue.async {
for _ in 0..<10 {
self.sellTicket()
}
}
}
//卖票
func sellTicket() {
OSSpinLockLock(&osspinlock)
var oldTicket = tickets
sleep(UInt32(0.2))
oldTicket -= 1
tickets = oldTicket
print("还剩\(tickets)张票 ---- \(Thread.current)")
OSSpinLockUnlock(&osspinlock)
}
可以看到,最终的余票数量已经是正确的了,这里要注意的是osspinlock
需要做成全局变量或者属性,多个线程要用这同一把锁去加锁和解锁,如果每个线程各自生成锁,则达不到要加锁的目的了
那么自旋锁是怎么样做到加锁保证线程安全的呢?
先来介绍下让线程阻塞的两种方法:
忙等
:也就是自旋锁的原理,它本质上就是个while循环,不停地去判断加锁条件,自旋锁没有让线程真正的阻塞,只是将线程处在while循环中,系统CPU还是会不停地分配资源来处理while循环指令。真正阻塞线程
: 这是让线程休眠,类似于Runloop里的match_msg()
实现的效果,它借助系统内核指令,让线程真正停下来处于休眠状态,系统的CPU不再分配资源给线程,也不会再执行任何指令。系统内核用的是symcall
指令来让线程进入休眠
它的原理就是,自旋锁在加锁失败时,让线程处于忙等状态,让线程停留在临界区之外,一旦加锁成功,就可以进入临界区对资源进行操作。
通过这个可以看到,苹果在iOS10之后就弃用了OSSpinLock
,官方建议用 os_unfair_lock
来代替,那么为什么要弃用呢?因为在iOS10之后线程可以设置优先级,在优先级配置下,可以产生优先级反转,使自旋锁卡住,自旋锁本身已经不再安全。
2. os_unfair_lock
os_unfair_lock
是苹果官方推荐的,自iOS10之后用来替代 OSSpinLock
的一种锁
os_unfair_lock_trylock
: 尝试加锁,加锁成功返回true,继续执行。加锁失败,则返回false,不会阻塞线程。os_unfair_lock_lock
: 加锁,加锁失败,阻塞线程继续等待。加锁成功,继续执行。os_unfair_lock_unlock
: 解锁
使用:
//卖票
func sellTicket() {
os_unfair_lock_lock(&unfairlock)
var oldTicket = tickets
sleep(UInt32(0.2))
oldTicket -= 1
tickets = oldTicket
print("还剩\(tickets)张票 ---- \(Thread.current)")
os_unfair_lock_unlock(&unfairlock)
}
打印结果和OSSpinLock
一样,os_unfair_lock
摒弃了OSSpinLock
的while循环实现的忙等状态,而是采用了真正让线程休眠,从而避免了优先级反转问题。
3. pthread_mutex
pthread_mutex
是pthread
跨平台的一种解决方案,mutex
为互斥锁,等待锁的线程会处于休眠状态。
互斥锁的初始化比较麻烦,主要为以下方式:
var ticketMutexLock = pthread_mutex_t()
- 初始化属性:
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
3. 初始化锁:pthread_mutex_init(&ticketMutexLock, &attr)
关于互斥锁的使用,主要提供了以下方法:
- 尝试加锁:
pthread_mutex_trylock(&ticketMutexLock)
- 加锁:
pthread_mutex_lock(&ticketMutexLock)
- 解锁:
pthread_mutex_unlock(&ticketMutexLock)
- 销毁相关资源:
pthread_mutexattr_destory(&attr)
,pthread_mutex_destory(&ticketMutexLock)
使用方式如下:
要注意,在析构函数中要将锁进行销毁释放掉
在初始化属性中,第二个参数有以下几种方式:
PTHREAD_MUTEX_DEFAULT
= PTHREAD_MUTEX_NORMAL
,代表普通的互斥锁PTHREAD_MUTEX_ERRORCHECK
代表检查错误锁PTHREAD_MUTEX_RECURSIVE
代表递归互斥锁
互斥锁的底层原理实现也是通过阻塞线程,等待锁的线程处于休眠状态,CPU
不再给等待的线程分配资源,和上面讲到的os_unfair_lock
原理类似,都是通过内核调用symcall
方法来休眠线程,通过这个对比也能推测出,os_unfair_lock
实际上也可以归属于互斥锁
3.1 递归互斥锁
如图所示,如果是上述场景,方法1里面嵌套方法2,正常调用时,输出应该为:
若要对上述场景保证线程安全,先用普通互斥锁添加锁试下
结果打印如下:
和预想中的不一样,如果懂得锁机制便会明白,图中所示的rsmText2中加锁失败,需要等待rsmText1中的锁释放后才可加锁,所以rsmText2方法开始等待并阻塞线程,程序无法再执行下去,那么rsmText1中锁释放的逻辑就无法执行,就这样造成了死锁,所以只能打印rsmText1中的输出内容。
解决这个问题,只需要给两个方法用两个不同的锁对象进行加锁就可以了,但是如果是针对于同一个方法递归调用,那么就无法通过不同的对象去加锁,这时候应该怎么办呢?递归互斥锁就该用上了。
如上,已经可以正常调用并加锁
那么递归锁是如何避免死锁的呢?简而言之就是允许对同一个对象进行重复加锁,重复解锁,加锁和解锁的次数相等,调用结束时所有的锁都会被解开
3.2 互斥锁条件 pthread_cond_t
互斥锁条件所用到的常见方法如下:
- 定义一个锁:
var condMutexLock = pthread_mutex_t()
- 初始化锁对象:
pthread_mutex_init(&condMutexLock)
- 定义条件对象:
var condMutex = pthread_cond_t()
- 初始化条件对象:
pthread_cond_init(&condMutex, nil)
- 等待条件:
pthread_cond_wait(&condMutex, &condMutexLock)
等待过程中会阻塞线程,知道有激活条件的信号发出,才会继续执行 - 激活一个等待该条件的线程:
pthread_cond_signal(&condMutex)
- 激活所有等待该条件的线程
pthread_cond_broadcast(&condMutex)
- 解锁:
pthread_mutex_unlock(&condMutexLock)
- 销毁锁对象和销毁条件对象:
pthread_mutex_destroy(&condMutexLock)
pthread_cond_destroy(&condMutex)
下面设计一个场景:
- 在一个线程里对dataArr做remove操作,另一个线程里做add操作
- dataArr为0时,不能进行删除操作
@IBAction func mutexCondTest(_ sender: Any) {
initMutextCond()
}
func initMutextCond() {
//初始化属性
var attr = pthread_mutexattr_t()
pthread_mutexattr_init(&attr)
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL)
//初始化锁
pthread_mutex_init(&condMutexLock, &attr)
//释放属性
pthread_mutexattr_destroy(&attr)
//初始化cond
pthread_cond_init(&condMutex, nil)
_testDataArr()
}
func _testDataArr() {
let threadRemove = Thread(target: self, selector: #selector(_remove), object: nil)
threadRemove.name = "remove 线程"
threadRemove.start()
sleep(UInt32(1))
let threadAdd = Thread(target: self, selector: #selector(_add), object: nil)
threadAdd.name = "add 线程"
threadAdd.start()
}
@objc func _add() {
//加锁
pthread_mutex_lock(&condMutexLock)
print("add 加锁成功---->\(Thread.current.name!)开始")
sleep(UInt32(2))
dataArr.append("test")
print("add成功,发送条件信号 ------ 数组元素个数为\(dataArr.count)")
pthread_cond_signal(&condMutex)
//解锁
pthread_mutex_unlock(&condMutexLock)
print("解锁成功,\(Thread.current.name!)线程结束")
}
@objc func _remove() {
//加锁
pthread_mutex_lock(&condMutexLock)
print("remove 加锁成功,\(Thread.current.name!)线程开启")
if(dataArr.count == 0) {
print("数组内没有元素,开始等待,数组元素为\(dataArr.count)")
pthread_cond_wait(&condMutex, &condMutexLock)
print("接收到条件更新信号,dataArr元素个数为\(dataArr.count),继续向下执行")
}
dataArr.removeLast()
print("remove成功,dataArr数组元素个数为\(dataArr.count)")
//解锁
pthread_mutex_unlock(&condMutexLock)
print("remove解锁成功,\(Thread.current.name!)线程结束")
}
deinit {
// pthread_mutex_destroy(&ticketMutexLock)
pthread_mutex_destroy(&condMutexLock)
pthread_cond_destroy(&condMutex)
}
输出结果为:
从打印结果来看,如果不满足条件时进行条件等待 pthread_cond_wati
,remove 线程是解锁,此时线程是休眠状态,然后等待的add 线程进行加锁成功,处理add的逻辑。
当add 操作完毕时,通过 pthread_cond_signal
发出信号,remove线程收到信号后被唤醒,然后remove线程会等待add线程解锁后,再进行加锁处理后续的逻辑.
整个过程中一共用到了三次加锁,三次解锁,这种锁可以处理线程依赖的场景.
4. NSLock, NSRecursiveLock, NSCondition
上文中提到了mutex普通互斥锁
mutex递归互斥锁
与 mutext条件互斥锁
,这几种锁都是基于C语言的API,苹果在此基础上做了面向对象的封装,分别对应如下:
NSLock
封装了pthread_mutex_t
的 attr类型为PTHREAD_MUTEX_DEFAULT
或者PTHREAD_MUTEX_NORMAL
普通锁NSRecursiveLock
封装了pthread_mutex
的 attr类型为PTHREAD_MUTEX_RECURSIVE
递归锁NSCondition
封装了pthread_mutex_t
和pthread_cond_t
底层实现和 pthread_mutex_t
一样,这里只看下使用方式即可:
4.1 NSLock
//普通锁
let lock = NSLock()
lock.lock()
lock.unlock()
4.2 NSRecursiveLock
let lock = NSRecursiveLock()
lock.lock()
lock.unlock()
4.3 NSCondition
let condition = NSCondition()
condition.lock()
condition.wait()
condition.signal()//condition.broadcast()
condition.unlock()
4.4 NSConditionLock
这个是NSCondition
的进一步封装,该锁允许我们在锁中设定条件具体条件值,有了这个功能,我们可以更加方便的多条线程的依赖关系和前后执行顺序
下面用一个场景来模拟下顺序控制的功能,有三条线程执行A,B,C三个方法,要求按A,C,B的顺序执行
@IBAction func conditionLockTest(_ sender: Any) {
let threadA = Thread(target: self, selector: #selector(A), object: nil)
threadA.name = "ThreadA"
threadA.start()
let threadB = Thread(target: self, selector: #selector(B), object: nil)
threadB.name = "ThreadB"
threadB.start()
let threadC = Thread(target: self, selector: #selector(C), object: nil)
threadC.name = "ThreadC"
threadC.start()
}
@objc func A() {
conditionLock.lock()
print("A")
sleep(UInt32(1))
conditionLock.unlock(withCondition: 3)
}
@objc func B() {
conditionLock.lock(whenCondition: 2)
print("B")
sleep(UInt32(1))
conditionLock.unlock()
}
@objc func C() {
conditionLock.lock(whenCondition: 3)
print("C")
conditionLock.unlock(withCondition: 2)
}
输出结果为:
A
C
B
5. dispatch_semaphore
信号量
的初始值可以用来控制线程并发访问的最大数量,初始值为1,表示同时允许一条线程访问资源,这样可以达到线程同步的目的
创建信号量:
dispatch_semaphore_create(value)
等待:
dispatch_semaphore_wait(semaphore, 等待时间)
信号量的值 <= 0,线程就休眠等待,直到信号量 > 0,如果信号量的值 > 0,则就将信号量的值递减1,继续执行下面的程序信号量值+1:
dispatch_semaphore_signal(semaphore)
链接:https://juejin.cn/post/7263350491610726460
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。