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

1

9

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

Logography answered 14/4, 2021 at 20:3 Comment(6)
Thanks for all the code, but can you provide a minimal reproducible example? What would we have to do in order to test?Loftin
I added a link to the project in the question. It includes the code examples mentioned in the question + a test target with a test that simulates the scenario. Worth mentioning again, that I didn't manage to reproduce the issue with this project and unfortunately I cannot share the original code.Logography
I suppose this happens because the objc_sync_* methods require their argument to have an identity. Structs don't have that, only classes.Latin
@Latin but self in the lock refers to the SwiftLock not AtomicLogography
I believe this post — entitled "objc_sync_enter / objc_sync_exit not working with DISPATCH_QUEUE_PRIORITY_LOW" — is sufficient to answer the question. Look at Sir Wellington's answer in the discussion. Basically, the issue arises because Swift structs are value types. It was also recommended to use GCD instead of objc_sync_enter in that same discussion as the latter is old and very low-levelHenceforward
GCD didn't work for me because a) I need the lock to be recursive b) synchronous, for those 2 reasons I'm doomed to a deadlock or a failed lock (concurrent execution). However, I have tried replacing SwiftLock implementation to use NSRecursiveLock (check update 2) and the issue persists and still fixed by changing the Atomic property wrapper declaration from struct to class`.Logography
L
2

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.

Logography answered 13/6, 2021 at 8:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.