Shaula Blog

美丽的东西都是肤浅的

iOS 并发编程的新选择:OSAllocatedUnfairLock

在 iOS 并发编程中,os_unfair_lock 以其出色的性能,自 iOS 10 起成为替代已废弃 OSSpinLock 的重要底层同步原语。然而,随着 Swift 语言与并发模型的演进,开发者在直接使用这一底层 C API 时,往往会遇到一些与 Swift 值语义、存储地址稳定性以及 API 易用性相关的问题。

其中最核心的问题并不只是“值拷贝”本身,而是:

os_unfair_lock 这类底层锁依赖稳定的内存地址,而 Swift 的值语义与复制/传递机制并不天然适合直接承载这类按地址操作的同步原语。

os_unfair_lock 是一个 C 结构体,在 Swift 中表现为值类型。理论上,一旦它被复制,就会产生新的独立存储;此时,不同副本上的加锁操作彼此并不互斥,因为它们已经不是同一把锁。更广义地说,只要代码无法明确保证始终操作的是同一块底层锁存储,线程安全就可能被破坏。

这也是为什么直接在 Swift 中把 os_unfair_lock 当作一个普通值类型来传递、捕获或嵌入各种值语义结构里,往往并不可靠。

传统的规避方案通常有两类:

  1. 将锁封装在引用类型中,借助引用语义保证底层锁存储的稳定性;
  2. 手动堆分配底层锁对象,通过指针显式管理其生命周期。

例如,手动堆分配的做法虽然可行,但会引入额外的内存管理复杂度:

import os

final class ManualLock {
    private let lockPtr: UnsafeMutablePointer<os_unfair_lock>

    init() {
        lockPtr = .allocate(capacity: 1)
        lockPtr.initialize(to: os_unfair_lock())
    }

    func withLock<R>(_ body: () throws -> R) rethrows -> R {
        os_unfair_lock_lock(lockPtr)
        defer { os_unfair_lock_unlock(lockPtr) }
        return try body()
    }

    deinit {
        lockPtr.deinitialize(count: 1)
        lockPtr.deallocate()
    }
}

这种方案的问题不在于不能用,而在于它过于底层:需要开发者自行保证生命周期、解锁时机与封装边界,既繁琐,也更容易出错。

OSAllocatedUnfairLock:更适合 Swift 的封装

为改善直接使用 os_unfair_lock 时的工程体验,Apple 在较新的系统版本中提供了 OSAllocatedUnfairLock。它的目标,是为 Swift 提供一个更适合现代语言语义的高性能互斥锁封装。

从使用者角度看,OSAllocatedUnfairLock 的价值主要体现在以下几点:

  1. 稳定的底层锁存储
    它避免了直接把底层锁当作普通 Swift 值到处传递所带来的身份与地址问题。开发者可以更安全地赋值、传参或闭包捕获,而不必自己处理底层锁存储的分配与管理。

  2. 自动生命周期管理
    无需像 UnsafeMutablePointer 方案那样手动申请和释放底层内存,降低了出错风险。

  3. 更适合 Swift 的 API 设计
    它提供了 withLock 这类基于作用域的接口,使“加锁—执行临界区—解锁”成为一个结构化操作,更符合 Swift 的错误处理与资源管理风格。

  4. 适合并发上下文使用
    它被设计为可在现代 Swift 并发环境中使用,相比直接操作底层 C 锁更自然。

简而言之,OSAllocatedUnfairLock 的意义不只是“替代一个旧 API”,而是把一个性能优秀但不够 Swift 友好的底层锁,包装成了更容易正确使用的现代接口。

基本用法

使用 withLock 管理临界区

OSAllocatedUnfairLock 最常见的使用方式是 withLock(_:)。它接收一个闭包,并保证闭包执行期间锁处于持有状态。

import os

final class DataCache {
    private let lock = OSAllocatedUnfairLock()
    private var storage: [String: Data] = [:]

    func set(_ data: Data, for key: String) {
        lock.withLock {
            storage[key] = data
        }
    }

    func get(_ key: String) -> Data? {
        lock.withLock {
            storage[key]
        }
    }
}

这种基于闭包的 API 相比手动调用底层加解锁有几个明显优势:

  • 错误路径更安全:即使闭包提前返回,或抛出错误,锁也能在作用域结束时被正确释放;
  • 临界区边界清晰:加锁范围由闭包天然界定,代码可读性更好;
  • 支持返回值:闭包可以直接返回结果,调用代码往往更简洁。

如果临界区中的代码可能抛出错误,withLock 也可以自然配合 throws 使用:

func loadValue() throws -> Int {
    try lock.withLock {
        // 访问受保护状态
        return 42
    }
}

尝试加锁

对于不希望阻塞当前执行路径的场景,可以使用 withLockIfAvailable(_:)。它只会在成功获取锁时执行闭包;如果当前锁不可用,则返回 nil

if let value = lock.withLockIfAvailable({
    computeValueFromSharedState()
}) {
    handleValue(value)
} else {
    performFallbackTask()
}

这种方式适合对时延敏感、或可以接受降级逻辑的场景。

总结

OSAllocatedUnfairLock 是 Apple 为现代 Swift 并发编程提供的重要改进之一。它解决的关键问题,不只是“少写几行样板代码”,而是把一个依赖稳定存储地址的底层锁原语,以更适合 Swift 的方式暴露出来。

它的价值主要在于:

  • 避免直接使用 os_unfair_lock 时常见的值语义与存储地址问题;
  • 降低手动管理底层锁存储的复杂度;
  • 通过 withLock 等作用域化 API 提高正确性与可读性;
  • 在大多数场景下仍保持非常优秀的性能表现。

对于部署目标为 iOS 16 及以上、且确实需要使用互斥锁保护共享可变状态的项目,OSAllocatedUnfairLock 通常是比直接操作 os_unfair_lock 更推荐的选择。