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

tru*_*pin 5 multithreading lazy-loading double-checked-locking swift swift4

我已经实现了我认为在类中进行双重检查锁定以实现线程安全的延迟加载.

万一你想知道,这是我目前正在研究的DI库.

我正在谈论的代码如下:

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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

我认为我离这里的最终解决方案非常接近,但由于我不是分布式系统专家,所以我想确定一下.

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

所以我的两个问题是:

  • 这是100%线程安全的,如果没有,为什么?
  • 在Swift中有更有效的方法吗?

Rob*_*ier 7

IMO,这里的正确工具是os_unfair_lock.双重检查锁定的目的是避免完全内核锁定的代价.os_unfair_lock在无争议的情况下提供.它的"不公平"部分是它不会对等待线程作出承诺.如果一个线程解锁,则允许重新锁定而没有另一个等待线程获得机会(因此可能会饿死).在实践中使用非常小的临界区,这是不相关的(在这种情况下,您只是检查局部变量与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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

这说,根据我的经验,这种懒惰在移动应用程序中几乎总是没有根据的.如果变量非常昂贵,但可能永远不会访问,它实际上只会节省您的时间.有时在大规模并行系统中,能够移动初始化是值得的,但是移动应用程序存在于相当有限数量的核心上,因此通常没有一些额外的核心可以将其分流.我通常不会追求这个,除非你已经发现当你的框架在实时系统中使用时这是一个重大问题.如果您有,那么我建议您os_unfair_lock在显示此问题的实际用法中分析您的方法.我期待os_unfair_lock赢.

  • 为了确保你的 os_unfair_lock 和其他 C 锁被分配在堆上并且永远不会被移动或复制,你应该使用 UnsafeMutablePointer 来分配和初始化(然后取消初始化和取消分配)它们。否则,它们可能会意外停止工作,在极少数情况下崩溃,并泄漏内核资源,GCD 团队的 Philippe Hausler 表示:https://forums.swift.org/t/atomic-property-wrapper-for-standard-library /30468/18 (2认同)