Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Atomic property wrapper only works when declared as class, not struct

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

public class SwiftLock {

    init() { }

   public func sync<R>(execute: () throws -> R) rethrows -> R {
    objc_sync_enter(self)
    defer { objc_sync_exit(self) }
    return try 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 the publicVariable2 property here)

class Model {
    @Atomic var publicVariable: TimeInterval = 0
    @Atomic var publicVariable2: TimeInterval = 0
    var sessionDuration: TimeInterval {
        min(0, publicVariable - publicVariable2)
    }
}

Update 1: Full Xcode project: https://drive.google.com/file/d/1IfAsOdHKOqfuOp-pSlP75FLF32iVraru/view?usp=sharing

like image 554
Yousef Hamza Avatar asked Apr 14 '21 20:04

Yousef Hamza


1 Answers

This is question is answered in this PR: https://github.com/apple/swift-evolution/pull/1387

I think this is those lines that really explains it 💡

In Swift's formal memory access model, methods on a value types are considered to access the entire value, and so calling the wrappedValue getter formally reads the entire stored wrapper, while calling the setter of wrappedValue formally modifies the entire stored 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.

like image 50
Yousef Hamza Avatar answered Oct 23 '22 11:10

Yousef Hamza