I have implemented what I think is a double check locking in a class to achieve thread safe lazy loading.
Just in case you wondered, this is for a DI library I'm currently working on.
The code I'm talking about is 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
}
}
The logic is to allow concurrent reads of isSet
so the accesses to instance
can be run in parallel from different threads. To avoid race conditions (that's the part I'm not 100% sure), I have two barriers. One when setting isSet
and one when setting instance
. The trick is to unlock the later only after isSet
is set to true, so the threads waiting on for instanceLocker
to be unlocked gets locked a second time on isSet
while it's being asynchronously written on the concurrent dispatch queue.
I think I'm very close from a final solution here, but since I'm not a distributed system expert, I'd like to make sure.
Also, using a dispatch queue wasn't my first choice because it makes me think reading isSet
isn't super efficient, but again, I'm no expert.
So my two questions are:
- Is this 100% thread-safe and if not, why?
- Is there a more efficient way of doing this in Swift?
body()
be serialized? Isbody()
not promised to be reentrant? Or is there another goal. Forget aboutisSet
; what isvalue
supposed to do here? (There is almost never a correct use of NSLock in Swift, ever. NSLock stopped being the right tool when GCD was released, long before Swift was created.) – JohnjohnaisSet
is exactly right (I just don't think you needisSet
) – Johnjohnainstance
to be blocking the thread knowing it's not necessary 99% of the time. IfNSLock
is wrong, I guess I could use aDispatchSemaphore
instead. I can't dispatchbody()
on a queue because I want it to be called on the same thread than the caller. – Houseleeklazy var
isn't thread safe yet, there's a ticket for that (bugs.swift.org/browse/SR-1042) which is still opened. And things likedispatch_once
doesn't exist in Swift (I'm not sure I could have used it here anyway...) – Houseleek