I am trying to implement a thread-safe lazy property wrapper on the grounds that achieving the thread-safe initialization code and correct handling a situation where multiple threads can enter the initialization code should be the solution. Here's the code of my property wrapper:
@propertyWrapper
public final class LazyThreadSafe<T> {
private let handler: () -> T
private var computedValue: T?
private let lock = NSLock()
public lazy var wrappedValue: T = lock.transaction {
if computedValue == nil {
computedValue = handler()
}
return computedValue!
}
public init(wrappedValue: @autoclosure @escaping () -> T) {
self.handler = wrappedValue
}
}
I'm also trying to take advantage of the lazy initialization built into Swift, which runs the initialization code only once if the property has not yet been initialized, thus avoiding the overhead of going through locking code after the property is initialized. One trick to note in the code is that when subsequent threads get to the initialization code after the initialization, they are given a cached value (computedValue) without re-running the initialization code.
When using exactly this approach but without a property wrapper, my thorough tests (including race condition tests) pass successfully. But with a property wrapper, the testing quite often ends up crashing with the "malloc: Double free of object" error.
I'm wondering if I'm on the wrong track because of contradictions with Swift's design, or is it some sort of current Swift limitations?
While you are waiting for full blown answer, I just want to say that lazy var isn't thread safe. Period. Even in your property wrapper it isn't thread safe as far as I can tell.
As for the thread-safety of lazy properties, the official documentation only states:
If a property marked with the lazy modifier is accessed by multiple threads simultaneously and the property hasn’t yet been initialized, there’s no guarantee that the property will be initialized only once.
My approach takes that into account. So far it withstands thorough thread-safety testing and if implemented on plain lazy instance properties it works flawlessly at all. Its logic is congruent with the intuitive concurrency logic. The only doubtful place left is whether the internal assignment operation is atomic with respect to the result of the initialization handler in all situations.
Thanks. As I said, my idea was to use Swift's built-in lazy behavior when a property is accessed multiple times - to reuse the cached value without calling the initialization handler. And all the added logic resides within the initialization handler itself.
The error message you see tells that an object tried to be deallocated twice, what exactly causes this crash I can't tell, may be somebody from compiler team knows. But my wild guess is that it something related to ARC, multithreading and lazy var initialised twice.
I saw exactly these kind of errors when fixed multiple "lazy var" related crashes in an application. The fix was replacing "lazy var" with custom thread-safe implementation.
Thanks, but where in your code is the lazy property? As I've said many times, my idea is based on leveraging it(!) And again, it works perfectly without property wrappers, on a regular lazy class property. The only question is whether such an approach is wrong, for example by Swift's design, or it's some current Swift limitation. But in any case, thanks for your help
You can't use the lazy keyword if you want thread safety. Period. Even if the initializer uses a lock, it's still not thread-safe.
There are plenty of other ways to implement lazy one-time initialization, as others on this thread have demonstrated. Why are you so obsessed with using the lazy keyword?
Thank you for your opinion. It would be really useful if you could provide some authoritative source, such as where it clearly states that lazy must not be used in a concurrent environment or at least some technical insight. As far as I know, not only does the official documentation say nothing about lazy initialization being completely non-threadsafe, but it even gives some hint as to where exactly you can expect the problem: "there is no guarantee that the property will only be initialized once” and that’s solvable. Could that specific property wrappers case be a bug, which should be fixed? Why make unbounded judgments?
I analyzed the SIL-code generated by the compiler for the lazy property, and essentially in Swift, it can be represented just like this:
class YYY {
var backingFoo: Int?
var foo: Int {
get {
lock()
let foo = backingFoo
unlock()
if let foo = foo {
return foo
} else {
let result = handler()
lock()
backingFoo = result
unlock()
return result
}
}
set {
lock()
backingFoo = newValue
unlock()
}
}
...
}
As you can see for yourself, the code is trivial and there is nothing in it that prevents you from using a thread-safe initializer approach.
While I get the appeal of this, we generally recommend against this pattern (concurrent queue + async for property access) because the overhead of spawning a thread to do the async if there isn't one active in the pool is many thousands of times slower than the work being done. The rule of thumb is async is only worth it with at least 100 microseconds of work, ideally 1ms or more. Additionally, DispatchQueue.sync cannot donate priority correctly with concurrent queues, though due to the previous issue that's less important here.
The problem is a bit tricky. A better way to grasp it is by looking at how it works under the hood (simplified). For example, this code:
lazy var data: [String] = lock.withLock {
... // some work
}
Behaves like this:
private var _data: [String]?
var data: [String] {
if _data == nil {
_data = lock.withLock {
... // some work
}
}
return _data!
}
The issue is that the lock only protects the closure execution, not the initialization check.
Since if _data == nil is outside the lock, multiple threads can pass this check at the same time, leading to duplicate initialization.