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.
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.