在 Swift 的多线程编程里,死锁是一个让人头疼的问题。要是处理不好,程序就可能陷入停滞,影响用户体验。接下来,咱就好好唠唠死锁的预防和解决办法。
一、啥是多线程编程里的死锁
多线程编程就是让程序里多个线程同时运行,这样能提高程序的效率。但有时候,多个线程会互相等待对方释放资源,谁也不让步,这就造成了死锁。打个比方,有两个人,一个拿着苹果,另一个拿着香蕉,他们都想同时拿到对方手里的东西,又都不肯先把自己的东西给对方,结果就僵持住了,这就是死锁。
在 Swift 里,下面这个例子就会造成死锁:
// 技术栈:Swift
import Foundation
// 创建一个串行队列
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
// 在主队列中执行闭包
DispatchQueue.main.async {
print("1. 进入主队列异步任务")
// 同步地在串行队列中执行闭包
serialQueue.sync {
print("2. 进入串行队列同步任务")
// 又想同步地在主队列中执行闭包
DispatchQueue.main.sync {
print("3. 进入主队列同步任务")
}
}
}
在这个例子里,主队列异步任务里的串行队列同步任务,又想同步地执行主队列任务,而主队列正在等串行队列任务完成,这就形成了死锁。
二、死锁出现的常见场景
1. 嵌套锁
当一个线程已经持有一个锁,又去尝试获取另一个锁,而另一个线程也这么做的时候,就容易出现死锁。比如:
// 技术栈:Swift
import Foundation
// 创建两个锁
let lock1 = NSLock()
let lock2 = NSLock()
// 线程 1
let thread1 = Thread {
lock1.lock()
print("线程 1 持有锁 1")
Thread.sleep(forTimeInterval: 1)
lock2.lock()
print("线程 1 持有锁 2")
lock2.unlock()
lock1.unlock()
}
// 线程 2
let thread2 = Thread {
lock2.lock()
print("线程 2 持有锁 2")
Thread.sleep(forTimeInterval: 1)
lock1.lock()
print("线程 2 持有锁 1")
lock1.unlock()
lock2.unlock()
}
// 启动线程
thread1.start()
thread2.start()
这里,线程 1 持有锁 1 后想获取锁 2,线程 2 持有锁 2 后想获取锁 1,就会造成死锁。
2. 同步调用
像上面第一个例子那种同步调用也容易导致死锁。主线程在等串行队列任务完成,而串行队列任务又想在主线程执行,就卡死了。
三、预防死锁的方法
1. 按顺序加锁
让所有线程都按照统一的顺序去加锁,这样就不会出现互相等待的情况。还是上面嵌套锁的例子,我们把顺序改成都先加锁 1 再加锁 2:
// 技术栈:Swift
import Foundation
// 创建两个锁
let lock1 = NSLock()
let lock2 = NSLock()
// 线程 1
let thread1 = Thread {
lock1.lock()
print("线程 1 持有锁 1")
lock2.lock()
print("线程 1 持有锁 2")
lock2.unlock()
lock1.unlock()
}
// 线程 2
let thread2 = Thread {
lock1.lock()
print("线程 2 持有锁 1")
lock2.lock()
print("线程 2 持有锁 2")
lock2.unlock()
lock1.unlock()
}
// 启动线程
thread1.start()
thread2.start()
这样,就不会出现死锁了。
2. 使用超时机制
加锁的时候设置一个超时时间,如果在规定时间内没拿到锁,就放弃。在 Swift 里可以通过 tryLock(before:) 方法来实现:
// 技术栈:Swift
import Foundation
let lock = NSLock()
let thread = Thread {
if lock.tryLock(before: Date(timeIntervalSinceNow: 1)) {
print("拿到锁了")
// 模拟一些操作
sleep(2)
lock.unlock()
} else {
print("没拿到锁,放弃操作")
}
}
thread.start()
3. 减少锁的使用
尽量少用锁,能不用就不用。可以用一些无锁的数据结构来替代,比如 ConcurrentDictionary 等。
四、解决死锁的方案
1. 检测死锁
可以用一些工具来检测死锁,像 Xcode 就自带了死锁检测功能。当程序出现死锁的时候,Xcode 会在调试控制台给出提示。
2. 重启线程或进程
如果检测到死锁,可以尝试重启相关的线程或者整个进程。不过这种方法比较暴力,可能会丢失一些数据。
3. 手动释放资源
在代码里手动释放一些锁资源,打破死锁的循环。比如在出现异常的时候,及时释放锁:
// 技术栈:Swift
import Foundation
let lock = NSLock()
let thread = Thread {
lock.lock()
do {
// 模拟一些可能出错的操作
if Bool.random() {
throw NSError(domain: "com.example.error", code: 1, userInfo: nil)
}
print("操作完成")
} catch {
print("出现错误:\(error)")
}
lock.unlock()
}
thread.start()
五、应用场景
多线程编程在很多场景下都会用到,比如网络请求、数据处理、UI 更新等。在网络请求的时候,为了不阻塞主线程,我们会开一个新的线程去处理请求,这时候就可能出现死锁。比如多个线程同时访问一个共享的网络连接资源,就容易造成死锁。
六、技术优缺点
优点
- 提高效率:多线程编程能让程序同时处理多个任务,提高程序的运行效率。
- 提升用户体验:不会让用户长时间等待,比如在处理复杂数据的时候,开新线程处理,主线程还能响应用户的操作。
缺点
- 死锁风险:就像我们前面说的,容易出现死锁问题。
- 调试困难:多线程程序的运行情况比较复杂,调试起来比较麻烦。
七、注意事项
- 线程安全:在多线程编程里,要保证数据的线程安全,避免出现数据竞争的问题。
- 锁的粒度:锁的使用范围要尽量小,不要把不必要的代码也锁起来,不然会影响程序的性能。
- 避免嵌套锁:尽量少用嵌套锁,嵌套锁很容易造成死锁。
八、文章总结
在 Swift 多线程编程中,死锁是一个需要重视的问题。我们要了解死锁出现的常见场景,像嵌套锁和同步调用等。预防死锁可以通过按顺序加锁、使用超时机制和减少锁的使用等方法。解决死锁可以用检测工具、重启线程或进程、手动释放资源等方案。同时,我们要清楚多线程编程的应用场景、优缺点和注意事项,这样才能编写出高效、稳定的多线程程序。
评论