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

时间:2018-06-10 00:45:10

标签: swift multithreading lazy-loading swift4 double-checked-locking

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

万一你想知道,这是我目前正在进行的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在并发调度队列上异步写入。

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

另外,使用调度队列并不是我的首选,因为它让我认为阅读 authorizer: arn: Fn::GetAtt: [UserPool, Arn] 不是超级高效的,但同样,我也不是专家。

所以我的两个问题是:

  • 这是100%线程安全的,如果没有,为什么?
  • 是否更有效率 在Swift中这样做的方式?

1 个答案:

答案 0 :(得分:7)

IMO,这里的正确工具是os_unfair_lock。双重检查锁定的目的是避免完全内核锁定的代价。 os_unfair_lock提供了无争议的案例。 &#34;不公平&#34;部分原因是它没有对等待线程作出承诺。如果一个线程解锁,则允许重新锁定而没有另一个等待线程获得机会(因此可能会饿死)。在实践中使用非常小的临界区,这是不相关的(在这种情况下,您只需检查局部变量为零)。它是一个比调度队列更低级的原语,速度非常快,但不如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能够获胜。