这里每天分享一个 iOS 的新知识,快来关注我吧
前言
前面讲了 swift 中的死锁,今天顺便聊聊 swift 中的线程安全相关的话题。
多线程并发是程序员都会用到的技能,但是因为调试和发现问题比较困难,所以往往最复杂和最奇怪的错误是由多线程并发引起的,这就需要我们在开发时特别注意。
在日常的开发中我们在处理多线程时应该多留个心眼,因为只要有多线程就会涉及到安全隐患。因此在本文中,我们将了解什么是线程安全,以及 iOS 提供了哪些工具来帮助我们实现它。
什么是线程安全?
线程安全指的是多个线程可能同时操作同一块内存,从而导致的异常情况,先举个例子:
swift
class User {
private(set) var name: String = ""
func setName(_ name: String) {
self.name = name
}
}
let user = User()
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
queue1.async {
user.setName("1")
print(user.name)
}
queue2.async {
user.setName("2")
print(user.name)
}
考虑下以上代码可能如何执行,测试下来可能会是打印了两个 2,这就不符合我们的预期了,明明第一个 user.setName
传入的是 "1",打印结果却为 2。
这种情况称为资源竞争,两个线程可能同时操作 user
对象,实际上,除了结果不符合预期外,还可能出现一个经典的崩溃 EXC_BAD_ACCESS
,这是因为让两个线程尝试同时操作同一个内存地址导致的。
解决竞争问题
由于是两个线程同时操作 user
导致的问题,那么我们想要解决,只需要让两个线程先后进行操作就行了,当线程 1 进行操作时,先上一把锁,锁住 user 不让线程 2 操作,等线程 1 操作完成,解开锁,再让线程 2 操作:
csharp
let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")
private let lock = NSLock()
queue1.async {
lock.lock()
user.setName("1")
print(user.name)
lock.unlock()
}
queue2.async {
lock.lock()
user.setName("2")
print(user.name)
lock.unlock()
}
这下打印就正常了,保证了线程安全。
其他并发问题
除了上边提到的资源竞争问题,在使用并发的时候还可能导致一些其他问题,也需要注意,比如:
-
条件竞争:无法同步执行两个或多个线程,导致事件以错误的顺序执行
-
死锁:两个线程相互等待,这意味着两者都无法继续,线程会卡死
-
优先级倒置:低优先级任务持有高优先级任务所需的资源,导致执行延迟
-
线程爆炸:程序中申请的线程数量过多,导致资源耗尽和系统性能下降
-
线程匮乏:因为其他线程正在占用这个资源,导致其他线程无法访问,从而导致执行延迟
iOS 中处理线程安全提供的 API
在 Swift 中,有许多不同的方法可以实现线程安全。具体要使用哪个 API 取决于你面临的问题,下边介绍一下这些 API,并提供一些示例来展示它们应该用于什么情况。
1、async/await
这个 API 在之前的文章中有介绍过:async/await
这个 API 是 swift 官方支持的,能够解决死锁、线程爆炸和数据竞争的问题,合理利用能够减少线程相关的问题,但不是杜绝,使用不合理的话一些逻辑错误仍有可能发生。而且该功能 iOS 15 开始才能使用,所以老项目目前还用不上。
2、使用串行队列
因为是多线程导致的问题,那么改成单线程,串行执行就能解决问题了, 比如上边的例子,改成单线程就可以了:
scss
let queue1 = DispatchQueue(label: "q1")
queue1.async {
user.setName("1")
print(user.name)
}
queue1.async {
user.setName("2")
print(user.name)
}
但是,有时候为了代码执行效率之类的衡量,又必须使用多线程,这时候这种方案就行不通了。
3、使用锁
我们最开始的解决方案就是用锁来解决的,锁在 iOS 中也提供了多种选择,比如:
-
pthread_mutex_t
一种阻塞锁,常见的表现形式是当前线程会进入休眠 -
pthread_rwlock_t
阻塞读写锁 -
DispatchQueue
可以用作阻塞锁,也可以封装为读写锁 -
OperationQueue
可以用作阻塞锁 -
NSLock
Objective-C 类的阻塞锁 -
OSSpinLock
自旋锁,使用一个无限循环直到锁被释放
我们一开始使用的解决方案就是 NSLock
,它 API 简单,易于使用且效率很高。
4、使用信号量
信号量(DispatchSemaphore)其实是一种可用来控制访问资源的数量的标识,它的原理类似自旋锁,当发起 wait
时,会先锁住资源不让其他人访问,直到释放信号量 signal
:
scss
let semaphore = DispatchSemaphore(value: 1)
queue1.async {
semaphore.wait()
user.setName("1")
print(user.name)
semaphore.signal()
}
queue2.async {
semaphore.wait()
user.setName("2")
print(user.name)
semaphore.signal()
}
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!