为什么要谈"死锁"
在 Swift 并发编程中,DispatchQueue.sync 以"阻塞式同步"著称:简单、直观、线程安全,却也最容易让生产环境直接崩溃。
什么是死锁(Deadlock)
| 维度 | 说明 |
|---|---|
| 定义 | 两个(或多个)执行单元互相等待对方释放资源,导致永远阻塞。 |
| 在 GCD 中的表现 | 线程 A 通过 sync 提交任务到队列 Q,而队列 Q 正在等待线程 A 完成 → 循环等待 → 触发 EXC_BAD_INSTRUCTION 崩溃。 |
| 常见结果 | 主线程卡死、App 秒退;Crash 日志中出现 0x8badf00d(应用无响应)或 EXC_I386_INVOP(非法操作)等错误码。 |
餐厅比喻
- waiter(服务员)同步下订单给 chef(厨师);
- chef 需要 waiter 回去问顾客口味,又同步派任务给 waiter;
- 两人互相等,餐厅停摆 → 死锁。
Swift 最小死锁示例
swift
import Foundation
// 1. 同队列嵌套 sync → 立即崩溃
let queue = DispatchQueue(label: "com.demo.queue")
queue.sync {
print("外层 sync")
queue.sync { // ❌ 在这里死锁
print("永远进不来")
}
}
运行后控制台只会打印 外层 sync,随后 App 崩溃。
原因:
- 外层闭包已占用队列唯一线程;
- 内层
sync要求同一条线程再次进入 → 无法满足 → 死锁。
双队列交叉死锁(更接近真实业务)
swift
let waiter = DispatchQueue(label: "waiter")
let chef = DispatchQueue(label: "chef")
// 模拟下单流程
waiter.sync {
print("① Waiter:同步下单给 Chef")
chef.sync {
print("② Chef:同步要求 Waiter 去问口味")
waiter.sync { // ❌ 交叉等待
print("③ Waiter:永远无法执行")
}
}
}
崩溃点:③ 处 waiter 队列已被①占用,而①又在等② → 循环等待。
如何"一键"解决------把任意一个 sync 改成 async
| 修改方案 | 代码片段 | 是否死锁 |
|---|---|---|
| ① → async | waiter.async { ... } |
❌ |
| ② → async | chef.async { ... } |
❌ |
| ③ → async | waiter.async { ... } |
❌ |
结论: 只要打破"循环等待链"中的任意一个环,死锁即刻解除。
在真实项目中,优先把"反向调用"做成 async 即可。
工程中最容易踩的"隐性死锁"
- 对外暴露 sync 接口
swift
class ImageCache {
private let queue = DispatchQueue(label: "cache")
private var storage: [String: UIImage] = [:]
// ❌ 危险:把内部队列 sync 暴露给外部
func read<T>(_ closure: () -> T) -> T {
return queue.sync(execute: closure) // 闭包里可能再调 read()
}
}
问题:调用方可能在闭包里再次调用 read() → 递归同步 → 死锁。
解决:
- 绝不对外暴露 sync;
- 主线程 sync 到主队列
swift
DispatchQueue.main.sync { // ❌ 100 % 死锁
// 代码永远不会进来
}
场景:在后台线程计算完后,想"立刻"回主线程刷新 UI,却手滑写成 sync。
正确姿势:
永远用 DispatchQueue.main.async { ... }。
sync 的正确打开方式------"私有队列 + 原子访问"
swift
/// 线程安全的日期格式化器缓存
final class DateFormatterCache {
private var formatters: [String: DateFormatter] = [:]
private let queue = DispatchQueue(label: "cache.\(UUID().uuidString)")
func formatter(using format: String) -> DateFormatter {
// 1. 只在此私有队列里同步,外部无法递归根除
return queue.sync { [unowned self] in
if let cached = formatters[format] {
return cached
}
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POSIX")
df.dateFormat = format
formatters[format] = df
return df
}
}
}
为什么这里不会死锁?
queue私有,外部无法直接往它塞 sync 任务;- 函数内部无递归调用;
- 闭包执行时间极短,不会阻塞用户可见线程。
checklist ✅
| 使用 sync 前自问 | 回答 |
|---|---|
| 队列是否私有? | ✅ |
| 闭包里还会 sync 到同队列吗? | ❌ |
| 阻塞是否影响主线程/用户滑动? | ❌ |
封装一个"防死锁"的读写锁
swift
/// 读写锁:写操作 barrier,读操作并发
final class RWLock<T> {
private var value: T
private let queue: DispatchQueue
init(_ initial: T) {
value = initial
queue = DispatchQueue(label: "rw.\(UUID().uuidString)", attributes: .concurrent)
}
// 读:并发
func read<U>(_ closure: (T) throws -> U) rethrows -> U {
try queue.sync { try closure(value) }
}
// 写:barrier
func write(_ closure: @escaping (inout T) -> Void) {
queue.async(flags: .barrier) { closure(&self.value) }
}
}
优点:
- 读并行、写串行;
- 外部无法拿到
queue引用,彻底杜绝递归 sync; - 所有写操作是
async,不会阻塞调用方。
总结------一句话记住
除非你在做原子访问,且队列私有、无递归,否则一律用 async。
扩展阅读 & 下一步
- 官方文档:DispatchQueue.sync
- WWDC 2022 -- Visualize and eliminate hangs with Instruments
- Swift Concurrency 时代:
- 用
actor替代"私有队列 + sync"; - 用
AsyncSequence做"异步回调链",天然避免死锁。
- 用