深入理解 DispatchQueue.sync 的死锁陷阱:原理、案例与最佳实践

为什么要谈"死锁"

在 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 即可。

工程中最容易踩的"隐性死锁"

  1. 对外暴露 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;
  1. 主线程 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。

扩展阅读 & 下一步

  1. 官方文档:DispatchQueue.sync
  2. WWDC 2022 -- Visualize and eliminate hangs with Instruments
  3. Swift Concurrency 时代:
    • actor 替代"私有队列 + sync";
    • AsyncSequence 做"异步回调链",天然避免死锁。

学习资料

  1. www.donnywals.com/understandi...
相关推荐
东坡肘子2 小时前
Skip Fuse现在对独立开发者免费! -- 肘子的 Swift 周报 #0110
android·swiftui·swift
Kapaseker21 小时前
Swift 构建 Android 应用?它来了
ios·swift
HarderCoder1 天前
Swift 协议(Protocol)指南(四):协议扩展(Protocol Extension)——让“协议”自己也有默认实现
swift
HarderCoder1 天前
Swift 协议(Protocol)指南(三):Primary Associated Type、some/any 与泛型式协议实战
swift
HarderCoder1 天前
Swift 协议(Protocol)指南(二):关联类型、Self 约束与泛型递归,一次彻底搞懂
swift
HarderCoder1 天前
Swift 协议(Protocol)指南(一):从语法到实战
swift
HarderCoder1 天前
Swift TaskGroup 结果顺序踩坑指南:为什么返回顺序和创建顺序不一致,以及最通用的修复办法
swift
Swift社区1 天前
iOS 基于 Foundation Model 构建媒体流
ios·iphone·swift·媒体
大熊猫侯佩2 天前
侠客行・iOS 26 Liquid Glass TabBar 破阵记
ios·swiftui·swift