深入理解 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...
相关推荐
汉秋19 小时前
SwiftUI 最新数据模型完整解析:@Observable、@State、@Bindable(iOS17+ 全新范式)
swiftui·swift
D***t13121 小时前
Swift在iOS中的多任务处理
开发语言·ios·swift
非专业程序员2 天前
iOS 实现微信读书的仿真翻页
ios·swiftui·swift
非专业程序员Ping2 天前
iOS 实现微信读书的仿真翻页
ios·swiftui·swift
xqlily4 天前
Swift:现代、高效、安全的编程语言(二)
swift
z***y8624 天前
Swift在iOS中的Xcode
ios·xcode·swift
songgeb5 天前
iOS Audio后台模式下能否执行非Audio逻辑
ios·swift
东坡肘子7 天前
毕业 30 年同学群:一场 AI 引发的“真假难辨”危机 -- 肘子的 Swift 周报 #112
人工智能·swiftui·swift
Antonio9157 天前
【Swift】UIKit:UIAlertController、UIImageView、UIDatePicker、UIPickerView和UISwitch
ios·cocoa·swift