I have created a "lock" in Swift and an Atomic property wrapper that uses that lock, for my Swift classes as Swift lacks ObjC's atomic property attribute.
When I run my tests with thread sanitizer enabled, It always captures a data race on a property that uses my Atomic property wrapper.
The only thing that worked was changing the declaration of the property wrapper to be a class instead of a struct and the main question here is: why it works
I have added prints at the property wrapper and lock inits to track the number of objects created, it was the same with struct/class, tried reproducing the issue in another project, didn't work too. But I will add the files the resembles the problem and let me know any guesses of why it works.
Lock
let lock = NSRecursiveLock()
public init() { }
public func sync<R>(execute: () -> R) -> R {
defer {
lock.unlock()
}
lock.lock()
return execute()
}
Atomic property wrapper
@propertyWrapper struct Atomic<Value> {
let lock: SwiftLock
var value: Value
init(wrappedValue: Value, lock: SwiftLock=SwiftLock()) {
self.value = wrappedValue
self.lock = lock
}
var wrappedValue: Value {
get {
lock.sync { value }
}
set {
lock.sync { value = newValue }
}
}
}
Model (the data race should happen on properties here)
class Model {
@Atomic var publicVariable: TimeInterval = 0
@Atomic var publicVariable2: TimeInterval = 0
var min: TimeInterval {
min(0, publicVariable - publicVariable2)
}
}
The Atomic property wrapper is class rather than a struct because the
memory guarded by synchronization primitives must be independent from
the wrapper. The wrapper's value will be loaded before the call to wrappedValue.getter and written back after the call to wrappedValue.setter. Therefore, synchronization within the wrapper
cannot provide atomic access to its own value. However, when the
wrapped value is a separate stored property within the wrapper object,
then it is accessed independently, only after calling the
wrappedValue's getter and setter. Note that a class type property
wrapper gives the wrapped value reference semantics. All copies of the
parent object will share the same atomic value.
Maybe take a look at this thread. I don’t know if it addresses your specific issue, but there’s good information about proper usage and potential pitfalls.
In that PR, I attempted to explain why the class implementation works with TSAN while the struct implementations fails. However, I don't want to advocate for using property wrappers to implement atomics. Even in the class implementation, it seems there could be a race between initialization of the wrapper and access to the property (however unlikely). I would feel better about completely removing the recommendation in SE-0258 to use property wrappers for atomics. Particularly now that there is a safe alternative: https://github.com/apple/swift-atomics
I don't think there's any need to be concerned about races between initialization and access. Initialization of an atomic really has to be ordered before all accesses to it, just like destruction has to be ordered after. It's possible to imagine very specific situations where those properties aren't necessary, but overwhelmingly they need to be true for overall correctness, and they're usually easy enough to guarantee.