在多线程的相关的开发中,必定会有锁的应用,这是因为如果多个线程极有可能会同时读取或者修改一个对象的值,那这时候很可能会出问题,比如读取的数值不对,或者出现之前对象的值已经被释放而引发野指针的问题
我们先看已经经典的售票问题,假设我们总共有500张票,有4个网络渠道同步售卖
先看不加锁的情况,并发4个线程来同时卖票,代码如下:
class ViewController: UIViewController {
var num = 10
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.global().async {
for _ in 0...10 {
self.buy()
}
}
DispatchQueue(label: "1",attributes: .concurrent).async {
for _ in 0...10 {
self.buy()
}
}
DispatchQueue(label: "2",attributes: .concurrent).async {
for _ in 0...10 {
self.buy()
}
}
for _ in 0...100 {
self.buy()
}
print("main thread完成任务")
}
func buy(){
if num > 0{
num -= 1
print("出票一张: 剩余: \(num)")
}
}
}
打印结果如下:
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0
注意看,这里的剩余票数不对,且只有10张票,出票了12次。
另外需要说明的是,因为是多线程的问题,所以随着并发次数增多,那错误会更多,比如10000张票,分10个渠道,分别尝试出票1000次,那结果会更加混乱,感兴趣的读者可以尝试
这里介绍下锁的概念,所谓的锁,就是当你操作这个变量的时候,把权限门锁上,等你开了锁之后,其他人才能操作这个变量,即保证同一时间只有一个操作行为
我们先试用比较基础的NSLock来加锁,NSLock是互斥锁,对应的是自旋锁。
互斥锁是指当一个线程进行访问的时候,该线程获得锁,其他线程进行访问的时候,将被操作系统挂起,直到该线程释放锁,其他线程才能对其进行访问,从而却确保了线程安全。
自旋锁是指线程在这一过程中保持执行,忙等待可以操作,比如每1ms来查看是否可以操作对象,该线程不会挂起,会一直占用cpu切片,但是没有互斥锁的线程切换行为
这两种锁各有利弊,但是一般还是推荐使用互斥锁,因为自旋锁用不好非常容易占用CPU资源
我们使用NSLock来进行加锁,首先我们定义一个锁 ,然后在出票前我们加锁,出票结束后我们解锁,代码如下:
class ViewController: UIViewController {
var num = 10
let lockLock = NSLock()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
DispatchQueue.global().async {
for _ in 0...10 {
self.buy()
}
}
DispatchQueue(label: "1",attributes: .concurrent).async {
for _ in 0...10 {
self.buy()
}
}
DispatchQueue(label: "2",attributes: .concurrent).async {
for _ in 0...10 {
self.buy()
}
}
for _ in 0...100 {
self.buy()
}
print("main thread完成任务")
}
func buy(){
lockLock.lock()
if num > 0{
num -= 1
print("出票一张: 剩余: \(num)")
}
lockLock.unlock()
}
}
输出日志如下:
出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7
出票一张: 剩余: 6
出票一张: 剩余: 5
出票一张: 剩余: 4
出票一张: 剩余: 3
出票一张: 剩余: 2
出票一张: 剩余: 1
出票一张: 剩余: 0
这里可以看到,顺序和剩余票数都对了
open class NSLock: NSObject, NSLocking {
internal var mutex = _MutexPointer.allocate(capacity: 1)
#if os(macOS) || os(iOS) || os(Windows)
private var timeoutCond = _ConditionVariablePointer.allocate(capacity: 1)
private var timeoutMutex = _MutexPointer.allocate(capacity: 1)
#endif
public override init() {
#if os(Windows)
InitializeSRWLock(mutex)
InitializeConditionVariable(timeoutCond)
InitializeSRWLock(timeoutMutex)
#else
pthread_mutex_init(mutex, nil)
#if os(macOS) || os(iOS)
pthread_cond_init(timeoutCond, nil)
pthread_mutex_init(timeoutMutex, nil)
#endif
#endif
}
deinit {
#if os(Windows)
// SRWLocks do not need to be explicitly destroyed
#else
pthread_mutex_destroy(mutex)
#endif
mutex.deinitialize(count: 1)
mutex.deallocate()
#if os(macOS) || os(iOS) || os(Windows)
deallocateTimedLockData(cond: timeoutCond, mutex: timeoutMutex)
#endif
}
open func lock() {
#if os(Windows)
AcquireSRWLockExclusive(mutex)
#else
pthread_mutex_lock(mutex)
#endif
}
open func unlock() {
#if os(Windows)
ReleaseSRWLockExclusive(mutex)
AcquireSRWLockExclusive(timeoutMutex)
WakeAllConditionVariable(timeoutCond)
ReleaseSRWLockExclusive(timeoutMutex)
#else
pthread_mutex_unlock(mutex)
#if os(macOS) || os(iOS)
// Wakeup any threads waiting in lock(before:)
pthread_mutex_lock(timeoutMutex)
pthread_cond_broadcast(timeoutCond)
pthread_mutex_unlock(timeoutMutex)
#endif
#endif
}
open func `try`() -> Bool {
#if os(Windows)
return TryAcquireSRWLockExclusive(mutex) != 0
#else
return pthread_mutex_trylock(mutex) == 0
#endif
}
open func lock(before limit: Date) -> Bool {
#if os(Windows)
if TryAcquireSRWLockExclusive(mutex) != 0 {
return true
}
#else
if pthread_mutex_trylock(mutex) == 0 {
return true
}
#endif
#if os(macOS) || os(iOS) || os(Windows)
return timedLock(mutex: mutex, endTime: limit, using: timeoutCond, with: timeoutMutex)
#else
guard var endTime = timeSpecFrom(date: limit) else {
return false
}
#if os(WASI)
return true
#else
return pthread_mutex_timedlock(mutex, &endTime) == 0
#endif
#endif
}
open var name: String?
}
这里可以看到,NSLock其实就是对pthread_mutex的封装,后面讲递归锁的时候,我们还会再仔细看这里的代码
刚才讲到NSLock是互斥锁,会挂起线程,那么如果在上面的代码最后,增加两次lock,那么主线程就会被挂起,增加代码如下:
lockLock.lock()
lockLock.lock()
print("main thread完成任务")
这里的main thread
完成任务永远不会执行了,非常恐怖
一个办法可以避免造成死锁,使用try函数,尤其是在主线程中加锁的时候,try函数的介绍如下:
Attempts to acquire a lock and immediately returns a Boolean value that indicates whether the attempt was successful.
if lockLock.try() {
lockLock.lock()
}
if lockLock.try() {
lockLock.lock()
}
if lockLock.try() {
lockLock.lock()
}
print("main thread完成任务")
这样子就不会发生死锁
所谓递归非递归,是指是否支持嵌套,比如Lock中如果还有这个lock,那是否会出问题,我们尝试下,比如用户1,他因为各种原因,会在买票行为过程前后加锁解锁,我们试下
DispatchQueue(label: "1",attributes: .concurrent).async {
print("用户1开始买票")
self.lockLock.lock()
for _ in 0...10 {
self.buy()
}
self.lockLock.unlock()
print("用户1结束买票")
}
我这里打印的结果是这样:
用户1开始买票
出票一张: 剩余: 9
出票一张: 剩余: 8
出票一张: 剩余: 7
这里就发生了死锁,用户1永远不会结束买票,因为它执行买票需要锁解除,而刚开始已经锁了,后续的解锁只能等购买行为结束才能解锁,购买行为需要等这个锁解除,就死循环了
怎么解决呢? 递归锁来解决,后续再增加这部分内容,所以这里要强调,lock一定要针对最小颗粒来进行解锁,避免死锁问题。 什么是最小颗粒?就是操作的最小函数,把操作相关的都封装函数,然后进行操作,不要在外部使用锁