在使用 dispatch_semaphore(信号量)时,最隐蔽且最常见的死锁类型是**“优先级反转导致的线程饥饿死锁”**,尤其是在 iOS 这种具有复杂优先级调度机制的系统中。
以下是该类死锁的详细成因及场景分析:
1. 优先级反转 (Priority Inversion) 引起的死锁
这是最隐蔽的一种。由于信号量不具备**“优先级继承”**机制(与 pthread_mutex 或 os_unfair_lock 不同),它会导致高优先级线程被无限期阻塞。
-
场景描述:
- 低优先级线程 (L) 持有了信号量资源。
- 中优先级线程 (M) 突然抢占了 CPU,开始执行大量计算任务。
- 高优先级线程 (H) 此时尝试
dispatch_semaphore_wait等待该信号量。
-
隐蔽结果:由于 L 的优先级低于 M,系统会优先让 M 执行。L 因为得不到 CPU 时间片而无法执行释放信号量 (
signal) 的操作。结果是:H 在等 L,L 在等 CPU,而 CPU 被 M 占用。从现象上看,H 线程直接卡死(死锁),但代码逻辑上并没有循环等待。
2. 同一串行队列下的循环等待
在使用信号量将异步操作“同步化”时,如果操作不当,极易在单一线程环境下造成死锁。
-
场景示例:
Objective-C
dispatch_semaphore_t sema = dispatch_semaphore_create(0); dispatch_async(dispatch_get_main_queue(), ^{ // 某些逻辑... dispatch_semaphore_signal(sema); // 希望在这里释放 }); dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // 在主线程阻塞等待 -
死锁原因:
wait操作直接阻塞了主线程(当前队列)。而signal被提交到了同一个队列的末尾。因为主线程被wait挂起,它永远无法执行到队列后方的signal代码块,从而导致永久死锁。
3. 嵌套调用导致的重入死锁
信号量是非递归的(Non-recursive) 。这意味着同一个线程如果尝试第二次获取它尚未释放的信号量,会立即把自己锁死。
-
场景描述:
在一个递归函数或者复杂的业务逻辑嵌套中,如果外部函数持有信号量,内部函数又尝试
wait同一个信号量,线程会进入自我等待状态。
总结与对比
| 特性 | dispatch_semaphore | pthread_mutex / os_unfair_lock |
|---|---|---|
| 优先级继承 | 不支持(易引发优先级反转死锁) | 支持(会自动提升低优先级线程) |
| 递归调用 | 不支持(重入即死锁) | 可配置支持(如 Recursive Mutex) |
| 隐蔽性 | 高(通常与系统调度算法耦合) | 较低(多为逻辑错误) |
建议:
在 iOS 开发中,除非是为了精确控制并发并发数(如 limit 为 N 的场景),否则在需要互斥锁时,优先推荐使用 os_unfair_lock。它不仅性能更高,且能有效避免信号量带来的优先级反转问题。