Sendable warning with thread safe property wrapper

When building with targeted strict concurrency, I'm getting a warning that I'm wondering if there is anything I can do about other than make my type "unchecked Sendable".

Consider some thread safe property wrapper:

@propertyWrapper struct ThreadSafe<Value: Sendable>: Sendable {
    private let lock: Lock<Value>

    var wrappedValue: Value {
        get { lock.withLock { $0 } }
        set { lock.withLock { $0 = newValue } }
    }
    
    init(wrappedValue: Value) {
        self.init(lock: .init(initialState: wrappedValue))
    }
    
    private init(lock: Lock<Value>) {
        self.lock = lock
    }
}

And consider the class that uses it to enforce thread-safety and sendability:

final class SomeSendable: Sendable {
    @ThreadSafe
    var someBool: Bool = false
}

Even though my class is thread-safe, I'm still getting this warning:

Stored property '_someBool' of 'Sendable'-conforming class 'SomeSendable' is mutable

It seems the compiler diagnostic isn't checking the property wrapper's Sendability.

Is there a way to appease this warning without making my type "unchecked"?

The problem is that any class with a var is always non-Sendable, and property wrappers don't allow let.

But really the problem, to me, is that SomeSendable really isn't all that safe to use, and in particular @ThreadSafe is not safe. It makes it far too easy to use mutable state in a way that is susceptible to races. Sure you won't get runtime crashes since the data is locked, but you can easily get incorrect results.

For example, something as simple as spinning up 1,000 tasks to toggle the boolean should always result in a true value at the end, but sometimes you get false and sometimes you get true:

let object = SomeSendable()
for _ in 1...1000 {
  Task { object.toggle() }
}
try await Task.sleep(for: .seconds(1))
print(object.someBool)

That's a pretty big problem, and it's happening because @ThreadSafe allows direct writing to the underlying value. So something like:

object.someBool = !object.someBool

…is hiding a race condition.

Really you should probably just hold onto the Lock value directly in your class rather than using the @ThreadSafe property wrapper, and then only mutate through it's withValue. And of course if safety of the mutable data is an utmost concern, then really you should probably use an actor.

6 Likes

Thank you for the reply. I see where you are coming from. I need to think through this a bit more.

Furthermore I'm not sure that a struct can be used to implement a concurrent data structure due to the law of exclusivity (mutating the struct is the same as having an inout on self, but please someone correct me if I'm wrong). I'm certainly getting thread-sanitizer errors when trying, using a class instead makes those errors go away.

This is a useful conversation topic about the broader question of correct implementation, but I agree with @mbrandonw that this is not really a good way to achieve your goals, as I discuss at @Atomic property wrapper for standard library? - #7 by lukasa.

1 Like

More broadly, the use-case you have is solved by OSAllocatedUnfairLock, which should be preferred over this hand-rolled solution.

2 Likes