4-19.【OC】【锁】使用 semaphore 容易制造哪类隐蔽死锁?

5 阅读2分钟

在使用 dispatch_semaphore(信号量)时,最隐蔽且最常见的死锁类型是**“优先级反转导致的线程饥饿死锁”**,尤其是在 iOS 这种具有复杂优先级调度机制的系统中。

以下是该类死锁的详细成因及场景分析:

1. 优先级反转 (Priority Inversion) 引起的死锁

这是最隐蔽的一种。由于信号量不具备**“优先级继承”**机制(与 pthread_mutexos_unfair_lock 不同),它会导致高优先级线程被无限期阻塞。

  • 场景描述

    1. 低优先级线程 (L) 持有了信号量资源。
    2. 中优先级线程 (M) 突然抢占了 CPU,开始执行大量计算任务。
    3. 高优先级线程 (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_semaphorepthread_mutex / os_unfair_lock
优先级继承不支持(易引发优先级反转死锁)支持(会自动提升低优先级线程)
递归调用不支持(重入即死锁)可配置支持(如 Recursive Mutex)
隐蔽性高(通常与系统调度算法耦合)较低(多为逻辑错误)

建议:

在 iOS 开发中,除非是为了精确控制并发并发数(如 limit 为 N 的场景),否则在需要互斥锁时,优先推荐使用 os_unfair_lock。它不仅性能更高,且能有效避免信号量带来的优先级反转问题。