一、线程安全场景
多个线程中一起拜访同一块资源,也便是资源共享。多牵扯到对同一块数据的读写操作,或许引发数据紊乱问题。
比较经典的线程安全问题有购票和存钱取钱问题,为了阐明读写操作引发的数据紊乱问题,以存钱取钱问题来做个阐明。
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)
- 初始化锁:
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)