class NonSendableClass {
var data: Int
init(data: Int) {
self.data = data
}
}
//@available(*, unavailable)
//extension NonSendableClass: Sendable { }
// Make Foo explicitly run on a different actor
actor SomeActor{
static func Foo(_ obj: NonSendableClass) async -> Int{
try? await Task.sleep(nanoseconds: UInt64.random(in: 0..<1000))
obj.data += 1
return 0
}
}
@MainActor
func main() async {
let nonSendableObject = NonSendableClass(data: 0)
let nonSendableObject2 = nonSendableObject
for i in 0...1000{
// SomeActor.Foo will increment this value by 1
let task = Task { await SomeActor.Foo(nonSendableObject) }
try? await Task.sleep(nanoseconds: UInt64.random(in: 0..<1000))
// Decrement this value by 1
nonSendableObject2.data -= 1
let _ = await task.value
// I would expect at the end of each iteration the value should be 0 +1 -1 = 0
print("Iteration \(i): \(nonSendableObject2.data)")
}
}
await main()
If you run this code multiple times you will see the result is not always 0, which implies there's data race.
I'm not sure what's the root cause for swift not being able to tell this is a potential data race but I've observed some interesting behaviors:
- With
swiftLanguageVersions: [.v5]
, there's no warning, but if I uncomment
//@available(*, unavailable)
//extension NonSendableClass: Sendable { }
then the compiler complains
"Passing argument of non-sendable type 'NonSendableClass' outside of main actor-isolated context may introduce data races; this is an error in the Swift 6 language mode"
which makes sense because I just marked NonSendableClass as not sendable, and Swift 5 doesn't have regional isolation it won't suppress the warning here (a bit more about this regional isolation later).
However if I comment these two lines, NonSendableClass should still be not sendable since it's a class but not final with mutable field data
, but this time no more warnings with Swift 5.
Btw I'm pretty sure NonSendableClass is not sendable because once I make it
final class NonSendableClass: Sendable {
the compiler explains
"Stored property 'data' of 'Sendable'-conforming class 'NonSendableClass' is mutable"
- With
swiftLanguageVersions: [.v6]
, there's still no warning, and even if I uncomment
//@available(*, unavailable)
//extension NonSendableClass: Sendable { }
there's still no warning. I suspect it's because of the regional isolation as explained here: `-strict-concurrency=complete` relaxes concurrency check. Compiler sees nonSendableObject is no longer used so it thinks it's safe to pass it to another actor, whereas nonSendableObject2 turns out to be pointing to the same object, making it unsafe and triggers the data race.
I think Swift 5's behavior is already problematic (not conservative enough) because it fails to decide NonSendableClass is not sendable if I don't explicitly say this, but at least it complains passing nonSendableObject
to SomeActor.Foo
if I explicitly declare NonSendableClass as not sendable.
But with Swift 6 it gets worse because even in this scenario the warning is gone, possibly due to regional isolation?
Since Swift 5 doesn't focus on concurrency safety, I know even if the compilation passes there could still be concurrency issues so I can bear with that. But now Swift 6 is really strict about concurrency safety so I guess I would expect myself at least seeing a warning somewhere in this code?