仔细检查锁优化以在 Swift 中实现线程安全的延迟加载

Double check lock optimization to implement thread-safe lazy-loading in Swift

我已经在 class 中实现了我认为的双重检查锁定,以实现线程安全延迟加载。

以防万一你想知道,这是我目前正在研究的DI library

我说的代码是the following:

final class Builder<I> {

   private let body: () -> I

   private var instance: I?
   private let instanceLocker = NSLock()

   private var isSet = false
   private let isSetDispatchQueue = DispatchQueue(label: "\(Builder.self)", attributes: .concurrent)

   init(body: @escaping () -> I) {
       self.body = body
   }

   private var syncIsSet: Bool {
       set {
          isSetDispatchQueue.async(flags: .barrier) {
             self.isSet = newValue
          }
       }
       get {
          var isSet = false
          isSetDispatchQueue.sync {
              isSet = self.isSet
          }
          return isSet
       }
   }

   var value: I {

       if syncIsSet {
           return instance! // should never fail
       }

       instanceLocker.lock()

       if syncIsSet {
           instanceLocker.unlock()
           return instance! // should never fail
       }

       let instance = body()
       self.instance = instance

       syncIsSet = true
       instanceLocker.unlock()

       return instance
    }
}

逻辑是允许并发读取 isSet,因此对 instance 的访问可以是 运行 从不同线程并行进行的。为了避免竞争条件(这是我不是 100% 确定的部分),我有两个障碍。设置 isSet 时一个,设置 instance 时一个。诀窍是仅在 isSet 设置为 true 后才解锁后者,因此等待 instanceLocker 解锁的线程在 isSet 上被异步写入时第二次被锁定并发调度队列。

我认为我离最终解决方案很近了,但由于我不是分布式系统专家,所以我想确定一下。

此外,使用调度队列不是我的第一选择,因为它让我觉得阅读 isSet 不是超级高效,但同样,我不是专家。

所以我的两个问题是:

IMO,此处正确的工具是 os_unfair_lock。双重检查锁定的要点是避免完全内核锁定的代价。 os_unfair_lock 规定在无争议的情况下。 "unfair" 的一部分是它不对等待线程作出承诺。如果一个线程解锁,则允许它重新锁定而另一个等待线程没有机会(因此可能会饿死)。实际上,对于一个非常小的关键部分,这是不相关的(在这种情况下,您只是检查一个局部变量是否为 nil)。它是比分派到队列更低级别的原语,队列非常快,但不如 unfair_lock 快,因为它依赖于像 unfair_lock.

这样的原语
final class Builder<I> {

    private let body: () -> I
    private var lock = os_unfair_lock()

    init(body: @escaping () -> I) {
        self.body = body
    }

    private var _value: I!
    var value: I {
        os_unfair_lock_lock(&lock)
        if _value == nil {
            _value = body()
        }
        os_unfair_lock_unlock(&lock)

        return _value
    }
}

请注意,您在 syncIsSet 上进行同步是正确的。如果您将其视为原语(这在其他双重检查同步中很常见),那么您将依赖 Swift 不承诺的东西(编写 Bools 的原子性和它会实际上检查布尔值两次,因为没有 volatile)。鉴于您正在进行同步,比较是在 os_unfair_lock 和调度到队列之间。

这就是说,根据我的经验,这种懒惰在移动应用程序中几乎总是没有根据的。如果变量非常昂贵但可能从未访问过,它实际上只会节省您的时间。有时在大规模并行系统中,能够移动初始化是值得的,但移动应用程序存在于相当有限的内核上,因此通常没有一些额外的内核可以将其分流到。我通常不会继续这样做,除非您已经发现当您的框架用于实时系统时这是一个严重的问题。如果你有,那么我建议在显示此问题的实际用法中针对 os_unfair_lock 分析你的方法。我希望 os_unfair_lock 获胜。