I was watching Protect Mutable State with Actors talk and at 22:42 it is mentioned about the Task.detached accepting a sendable closure which prevents data races
However when I tried to execute the same code I didn't get any compilation error.
Actual Behaviour
No compilation errors
Expected Behaviour
Compilation Error Mutation of captured var 'counter' in concurrently-executing code
Question
Am I missing something? Or has some API / concept changed?
Environment
Xcode 14.0 beta 2 (14A5229c)
macOS 13.0 Beta (22A5286j)
Xcode Command line project
Code
import Foundation
struct Counter {
var value = 0
mutating func increment () -> Int {
value = value + 1
return value
}
}
var counter = Counter ()
Task.detached {
print(counter.increment()) //No compilation error
}
Task.detached {
print(counter.increment()) //No compilation error
}
RunLoop.main.run()
Observation
When counter is defined in a Task { ... } then the warnings are thrown as shown in the comments.
Should it be wrapped in a Task { ... } for it to be caught? Is this a bug?
Task {
var counter = Counter ()
Task.detached {
print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
}
Task.detached {
print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
}
Task {
print(counter.increment()) //Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
}
}
No definitive answer here, but I think that's basically a problem with the counter in your example being a global variable. I further assume you're using a playground, correct?
I think for this race detection to work the compiler needs to know certain things related to the ownership of the counter and global and static properties simply do not get "caught" in this.
I'm not sure if this could/should be fixed in a general way, but at least the WWDC session should have somehow addressed this, I guess. The slides simply look like it would work copied as-is to a playground (where you're basically nudged into using lots of global variables). Maybe it is a bug in the way the compiler checks for this, but perhaps it's also not easy or even possible to do for global and static variables...
I briefly tested this in a SwiftUI project and that basically shows the same behavior. If the counter is constructed outside of the @main struct (i.e. as top-level code) or as a static property of some type the compiler does not catch the race condition. The latter (static property) can even be shown by adapting your example like so:
struct Counter {
var value = 0
mutating func increment () -> Int {
value = value + 1
return value
}
}
struct Foo {
static var counter = Counter()
}
Task {
var counter = Counter()
Task.detached {
print(Foo.counter.increment()) // no error
print(counter.increment()) // error
}
Task.detached {
print(Foo.counter.increment()) // no error
print(counter.increment()) // error
}
}
You were right looks like counter being a global variable had something that caused the compiler not to detect it. I wouldn't have guessed without your suggestion, thanks!
The following code throws the warnings: Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
import Foundation
struct Counter {
var value = 0
mutating func increment () -> Int {
value = value + 1
return value
}
}
do {
var counter = Counter ()
Task.detached {
//This is will be a data race in runtime
print(counter.increment()) //Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
}
Task.detached {
//This is will be a data race in runtime
print(counter.increment()) //Warning: Mutation of captured var 'counter' in concurrently-executing code; this is an error in Swift 6
}
}
RunLoop.main.run()
That's interesting, this was not an idea that came to me when testing, but it makes kind of sense.
I assume ultimately the entire question of whether something leads to a data race like this is tied to the scope of code as well, so above makes sense. Your last snippet basically creates a new scope just like any type instance does, in this case it's just an anonymous one.
Perhaps somebody more familiar with how concurrency is implemented could elaborate further on this here? Is it simply not possible for the compiler to analyze the examples we found that don't give warnings or is this an oversight? And, more importantly, will Swift 6 address this? Playgrounds are one thing, but command line apps are probably more common and important and it would be a shame if race conditions could still slip into people's projects like this...