I'm experimenting with Swift 6's new data-race detection and I noticed something confusing.
Given the following code:
class MutableClass {
var value: Int = 0 {
didSet {
print("Value is now \(value)")
}
}
}
func nonisolatedMethod(mutableClass: MutableClass) async {
mutableClass.value += 1
}
func nonisolatedMethod1(mutableClass: MutableClass) async {
print(mutableClass.value)
}
@MainActor
func test() {
let mutableClass = MutableClass()
for _ in 0 ... 100_000 {
Task {
await nonisolatedMethod(mutableClass: mutableClass)
}
Task {
await nonisolatedMethod1(mutableClass: mutableClass)
}
}
}
This code clearly performs concurrent reads and writes on mutableClass.value.
In theory, this should be a data race: one task modifies the property while another one reads it.
However, Swift 6 does not emit any warning or diagnostic here.
My question
Why doesn't Swift 6 detect or warn about a data race in this situation, given that:
MutableClass is a reference type (shared across tasks) and it’s not sendable
multiple Task {} blocks are accessing and mutating it concurrently
there is no synchronization and no actor protecting the shared state
I would expect Swift's data-race checker to report this, especially since removing @MainActor or using async letdoes produce diagnostics in similar scenarios.
The same code won’t compile if I have no MainActor on test method
Any explanation about why this does not trigger a data-race warning, or clarification about how Swift 6 determines when to report concurrency violations, would be greatly appreciated.
nobody around here can confirm or deny if or when anything in Xcode will ship, but given that it is in the 6.3 branch, that would be when i'd expect it (barring any future reversions, etc).
Both tasks are isolated to the main actor, and they don’t have any (real) suspension points, meaning they can’t be running simultaneously. I don’t understand why there sholud be a race here.
I agree with you: tasks are isolated on the MainActor and are suspended, but the tasks will run in parallel, and these methods will execute on a background thread in parallel.
so we will have on
Main thread nonisolatedMethod() and on bacgkound thread mutableClass.value += 1
because I am using nonIsolatedMethod and these code will be on background thread : mutableClass.value += 1
assuming the sample code is not using the NonisolatedNonsendingByDefault feature, then the two Tasks will be isolated to the main actor, but the calls to nonisolatedMethod and nonisolatedMethod1 will not be – when called, they must 'hop off' the current actor to execute. thus those methods can run concurrently with respect to each other, and since they share the same class reference and are reading/writing to its storage, that's the race.
if you were to mark the async methods as nonisolated(nonsending) though (explicitly, or by enabling the NonisolatedNonsendingByDefault feature), they would just run in the same isolation they're called from (MainActor in this example), and so the same race would not exist:
class MutableClass {
var value: Int = 0 {
didSet {
print("Value is now \(value)")
}
}
}
nonisolated(nonsending)
func nonisolatedMethod(mutableClass: MutableClass) async {
MainActor.assertIsolated()
mutableClass.value += 1
}
nonisolated(nonsending)
func nonisolatedMethod1(mutableClass: MutableClass) async {
MainActor.assertIsolated()
print(mutableClass.value)
}
@MainActor
func test() {
let mutableClass = MutableClass()
for _ in 0 ... 10 {
Task {
await nonisolatedMethod(mutableClass: mutableClass)
}
Task {
await nonisolatedMethod1(mutableClass: mutableClass)
}
}
}
test()
try? await Task.sleep(for: .seconds(1))
here's a godbolt example demonstrating the difference: Compiler Explorer
i was going to make a pedantic comment about this, stating that the Tasks do not run in parallel, it's just the calls to the async methods that do, but it got me thinking about what exactly it means for a Task to 'run'. is it 'running' while suspended calling an async function in a different isolation? so... i guess i'll just leave this here and maybe somebody more insightful can come up with more precise terminology.