【问题标题】:Double check lock optimization to implement thread-safe lazy-loading in Swift双重检查锁优化以在 Swift 中实现线程安全的延迟加载
【发布时间】:2018-06-10 00:45:10
【问题描述】:

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

以防万一您想知道,这是我目前正在处理的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 效率不高,但同样,我不是专家。

所以我的两个问题是:

  • 这是 100% 线程安全的吗?如果不是,为什么?
  • 有没有更高效的 在 Swift 中如何做到这一点?

【问题讨论】:

  • 这太复杂了。调度队列绝对是正确的工具; NSLock 是错误的工具。但是,我正在尝试弄清楚目标到底是什么。这里的目标是调用body() 被序列化吗? body() 不是承诺可重入的吗?还是有别的目标。忘记isSetvalue 应该在这里做什么? (在 Swift 中几乎从来没有正确使用过 NSLock。当 GCD 发布时,NSLock 不再是正确的工具,早在创建 Swift 之前。)
  • 双重检查锁定也永远不是 Swift 中的正确工具。 (在其他语言中很难做到正确,但甚至没有理由尝试在 Swift 中。)你也很接近。您在isSet 周围的代码完全正确(我只是认为您不需要isSet
  • 这里的目标是在第一次设置值时有一个屏障,然后在值被实例化后避免这个屏障。我不希望 instance 的读取在 99% 的情况下都没有必要阻塞线程。如果NSLock 错了,我想我可以改用DispatchSemaphore。我无法在队列中调度 body(),因为我希望它在与调用者相同的线程上被调用。
  • 为什么双重检查锁定在 Swift 中从来都不是正确的工具?我没有看到任何其他选项,因为 lazy var 还不是线程安全的,有一张仍然打开的票 (bugs.swift.org/browse/SR-1042)。而像 dispatch_once 这样的东西在 Swift 中不存在(我不确定我是否可以在这里使用它......)

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


【解决方案1】:

IMO,这里的正确工具是os_unfair_lock。双重检查锁定的目的是避免使用完整的内核锁定。 os_unfair_lock 在无争议的情况下提供了这一点。它的“不公平”部分是它没有承诺等待线程。如果一个线程解锁,则允许重新锁定而另一个等待线程没有机会(因此可能会饿死)。在使用非常小的临界区的实践中,这是不相关的(在这种情况下,您只是针对 nil 检查局部变量)。它是一个比调度到队列更低级别的原语,它非常快,但不如不公平锁快,因为它依赖于不公平锁等原语。

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 不承诺的东西(编写 Bool 的原子性和它实际上会检查 boolean两次,因为没有volatile)。鉴于您正在进行同步,比较是在 os_unfair_lock 和调度到队列之间。

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

【讨论】:

  • 谢谢!这很有帮助。我不知道os_unfair_lock
  • 我还在原来的解决方案中用DispatchSemaphore 替换了NSLock,效果很好。你知道DispatchSemaphoreNSLock 相比有多好吗?从文档看好像是优化的,但是没有具体描述是什么方式。
  • 无论如何,您的解决方案运行良好并且似乎是最优化的。到目前为止,我还没有显示这个问题的实际用法,但是由于我正在构建一个库,所以我正在尝试处理最坏的情况,因为我不知道它会如何被使用,我也不知道body()会有多重,因为它是图书馆用户提供的。
  • 您可能是对的,如果我希望开发人员有机会在大量多线程程序中使用我的库,那么延迟加载默认行为是错误的方向。我现在使用延迟加载的根本原因是我正在使用 API 构建一个依赖容器,该 API 允许注册使用参数的依赖构建器,这意味着我无法预先构建依赖,因为我没有参数前期。所以我想我要做的是提供一个额外的 API 来直接注册实例,它将构建部分委托给调用者。
  • 为确保您的os_unfair_lock 和其他C 锁分配在堆上并且从不移动或复制,您应该使用UnsafeMutablePointer 分配和初始化(然后取消初始化和解除分配)它们。否则,它们可能会意外停止工作,在极少数情况下崩溃,并泄漏内核资源,根据 GCD 团队的 Philippe Hausler 的说法:forums.swift.org/t/atomic-property-wrapper-for-standard-library/…
猜你喜欢
  • 1970-01-01
  • 2023-03-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-27
  • 1970-01-01
  • 2023-04-07
  • 1970-01-01
相关资源
最近更新 更多