在并发编程的语境下,虽然我们经常把 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. 实战建议:什么时候用它?
既然它不是严格的锁,我们就应该把它用在它擅长的地方:
- 控制并发流量: 比如限制同时进行的网络请求数量为 3。
- 跨线程通知(类似 Condition): 线程 A 完成某事后
signal,线程 Bwait到信号后再继续。 - 异步转同步: 在某些必须同步返回结果的旧接口中,等待异步回调完成。