【发布时间】: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()不是承诺可重入的吗?还是有别的目标。忘记isSet;value应该在这里做什么? (在 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