I am trying to use strict Swift 6 but unable to get this to work. Seems it should be easy. Not seeing how this could cause a data race.
@MainActor
class Controller: ObservableObject {
var game: Game?
@Published var state: State = .playing
@Published var size: Double
@Published var mines: Double
init() {
let size = 13
let mines = 28
self.size = Double(size)
self.mines = Double(mines)
Task {
let newGame = await Self.newGame(size: size, mines: mines) // Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
await MainActor.run {
self.game = newGame // Sending 'self' risks causing data races
}
}
}
static func newGame(size: Int, mines: Int) async -> Game {
// init game
}
}
"Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure"
I've tried different permutations of @MainActor, setting the task without MainActor.run, weak references, unchecked Sendable, and @Sendable. I would be fine with an escape hatch approach.
This code compiles for me without error. I did have to make a few assumptions, though. I made Game a non-Sendable class, and State a simple enum.
The error is confusing, because self, being a global actor isolated type, is definitely Sendable. And newGame will also be inferred to be MainActor because it is defined within a MainActor type.
Another thing. You can simplify the Task quite a bit:
// because your type is MainActor...
init() {
// ...this is MainActor too....
let size = 13
let mines = 28
self.size = Double(size)
self.mines = Double(mines)
Task {
// ... so this Task will inherit the same isolation
self.game = await Self.newGame(size: size, mines: mines)
}
}
So strange, and thanks for explanation, still having the issue. My game was a struct, I changed that to a class, my state is a simple enum, then I simplified it as you show (which is how I originally tried to do it). But still getting:
Passing closure as a 'sending' parameter risks causing data races between code in the current task and concurrent execution of the closure
OK. My mistake, in the example above, it was on @MainActor but that wasn't in my compiled code. So adding that to my controller fixed the problem. Thanks for help.
Is there a reason newGame has to be async? You can make this code satisfy the compiler in various ways, but at the end of the day, you probably want your controller to have an invariant where the Game value is properly set up.
Deciding what does and doesn’t make sense to be asynchronous is a big part of using concurrency! You probably want to be able to bring up your UI, render the current game state, and so on without having to block on external data sources. UI rendering is synchronous in most environments: there’s an event loop which serializes rendering and event handling so that you always render a consistent scene, and asynchrony would need to block that loop, which is usually seen as undesirable. That means you need a synchronous path from the UI to at least some copy of the game state that it can render. The simplest approach would be to make the UI own the canonical game state, but if you want to practice concurrency, you could also make that be owned by an actor that sends updated states to be rendered, receives moves from user events, eventually talks to a server, whatever.