4-16.【OC】【锁】为什么 dispatch_semaphore 不是严格意义上的锁?

1 阅读3分钟

在并发编程的语境下,虽然我们经常把 dispatch_semaphore 当作锁来用(比如信号量设为 1),但从设计哲学底层语义来看,它确实不是严格意义上的“锁”。

主要原因在于它缺乏锁的两个核心属性:所有权(Ownership)优先级继承(Priority Inheritance)


1. 缺乏所有权(No Ownership)

这是信号量与锁最本质的区别。

  • 锁(Lock): 具有强烈的“归属感”。如果线程 A 加了锁,那么必须由线程 B 等待,且必须由线程 A 释放。如果线程 B 尝试去解锁线程 A 的锁,通常会触发异常或未定义行为。
  • 信号量(Semaphore): 仅仅是一个计数器。线程 A 可以在信号量上执行 wait(减 1),而线程 B 可以在完全不持有该信号量的情况下执行 signal(加 1)。

工程影响: 这种特性让信号量更像是一种**“入场券” 。它不关心是谁发了票,谁用了票,只关心票的总数。这使得它适合用来控制并发频率**(如限制同时下载的任务数),而不是保护资源独占


2. 缺乏优先级继承(No Priority Inheritance)

这是信号量在 iOS 开发中最大的潜在风险点,也是它不能被称为“安全锁”的原因。

  • 互斥锁(如 os_unfair_lock): 具备优先级继承机制。如果一个高优先级线程在等待低优先级线程持有的锁,内核会临时提升低优先级线程的优先级,让它赶快干完活释放锁,从而避免“优先级反转”。
  • 信号量: 内核并不记录“谁”持有了信号量,因此当高优先级线程在 wait 时,内核无法得知应该去提升哪个线程的优先级。

后果: 可能会出现高优先级线程被无限期阻塞的情况,因为中间优先级的线程抢占了正在干活的低优先级线程,而信号量对此无能为力。


3. 语义重心不同:同步 vs. 互斥

  • 锁的语义是“互斥” (Mutual Exclusion): 目的是确保同一时间只有一个线程访问临界区。
  • 信号量的语义是“同步” (Synchronization): 目的是协调线程间的执行顺序。
特性互斥锁 (Mutex / Lock)信号量 (Semaphore)
关注点谁锁住了资源?还有多少资源可用?
释放者必须是加锁者可以是任何线程
递归性可能支持 (Recursive Lock)绝对不支持(会直接死锁)
内核优化拥有优先级继承优化仅作为原子计数器

4. 性能上的“欺骗性”

dispatch_semaphore 的性能非常高,因为它在没有竞争时完全在用户态执行(仅通过原子操作增减计数)。只有当需要阻塞线程时,才会陷入内核。

但这种高性能是以牺牲调度公平性安全性为代价的。苹果在 WWDC 中曾多次建议:如果是为了保护临界区(加锁),请优先使用 os_unfair_lock


5. 实战建议:什么时候用它?

既然它不是严格的锁,我们就应该把它用在它擅长的地方:

  1. 控制并发流量: 比如限制同时进行的网络请求数量为 3。
  2. 跨线程通知(类似 Condition): 线程 A 完成某事后 signal,线程 B wait 到信号后再继续。
  3. 异步转同步: 在某些必须同步返回结果的旧接口中,等待异步回调完成。