4-6.【OC】【锁】@synchronized 在编译期会被展开成什么?

3 阅读3分钟

在 Objective-C 中,@synchronized(obj) 是最易用的同步手段,但它其实是一个高度封装的语法糖。在编译期,编译器会将它展开为一对特殊的函数调用,并配合异常处理机制来确保锁的释放。


1. 编译后的核心展开

当你写下:

Objective-C

@synchronized(obj) {
    // 临界区代码
}

编译器(LLVM)会将其转换成类似下面的伪代码:

C++

// 1. 准备工作
id _reltarget = (id)obj;

// 2. 加锁
objc_sync_enter(_reltarget);

// 3. 开启异常处理 (Try-Catch)
// 这样做是为了保证即便临界区崩溃,锁也能被释放,防止死锁
try {
    // 你的业务代码
} finally {
    // 4. 解锁
    objc_sync_exit(_reltarget);
}

2. 核心函数解析

底层的真正功臣是 objc-sync 源码中的两个函数:

objc_sync_enter(obj)

  • 它会根据 obj 的内存地址,从一个全局的哈希表中找出一个对应的递归锁(Recursive Lock)。
  • 为什么要递归锁? 因为 @synchronized 支持嵌套调用(即同一个线程可以多次对同一个对象加锁而不死锁)。
  • 如果 objnil,该函数会静默失败(什么都不做)。这也是为什么 @synchronized(nil) 无法起到同步作用的原因。

objc_sync_exit(obj)

  • 对应的解锁操作。它必须与 enter 成对出现。
  • 由于编译器将其包裹在 finally 块中,即使你在代码块里写了 return 或者发生了运行时异常,系统也能保证调用 exit

3. 底层存储:SyncData 结构

@synchronized 并没有在你的对象 obj 内部增加一个锁字段,而是维护了一个全局的 StripedMap<SyncList> 缓存。

  • SyncData: 每个被锁的对象都会对应一个 SyncData 结构体,里面包含该对象本身、一个递归锁 recursive_mutex_t,以及指向下一个 SyncData 的指针。
  • 性能权衡: 由于多个对象可能哈希到同一个桶(Bucket),在高并发下修改这个全局哈希表会有一定的竞争开销。这就是为什么它的性能通常不如 os_unfair_lockdispatch_semaphore

4. 为什么 Swift 抛弃了它?

Swift 追求显性(Explicit)和性能,而 @synchronized 存在以下缺陷:

  1. 隐式开销: 强制引入了异常处理逻辑(Try-Catch),在 Swift 的错误处理模型中这太重了。
  2. 全局锁竞争: 全局哈希表的查找和维护带来了额外的性能损耗。
  3. 安全性隐患: 开发者容易误用 nil 对象导致锁失效,且这种错误很难在编译期发现。

Swift 的替代方案:

在 Swift 中,我们通常使用更轻量、更明确的方式:

  • os_unfair_lock: 性能最高,适合极其简单的临界区。
  • DispatchQueue: 通过串行队列实现同步。
  • Actors: Swift 5.5+ 推荐的并发模型,从语言层面消除数据竞争。

总结

@synchronized 在编译期被展开为 objc_sync_enter + try-finally + objc_sync_exit。它通过对象地址在全局哈希表中查找递归锁。虽然好用,但因为全局竞争和异常处理的开销,在追求极致性能的现代 iOS 开发中已不再是首选。