mattie
1
I'm trying to figure out the difference between these two paths. One produces a diagnostic and the other does not. But, I'm struggling to figure out why. Both are isolated to the MainActor at the continuation.resume call site.
Note I know this is a continuation misuse. The question is about the difference in diagnostics specifically.
struct Value {
let body: Any
}
struct Bar {
func send(completion: @escaping (Value) -> Void) {
let response = Value(body: "bar")
completion(response)
}
}
@MainActor
class MainActorClass {
let bar = Bar()
func getResponse() async -> Value {
await withCheckedContinuation { continuation in
bar.send() { response in
// error: sending response risks races
continuation.resume(returning: response)
Task {
// yet somehow no risk of that here?
continuation.resume(returning: response)
}
}
}
}
}
2 Likes
vns
2
I was trying to reason if that's something in Task initializer that causes such behaviour (@_inheritActorContext or @isolated(any)), but nothing seems to affect it when added to send.
There is a note, however:
- note: main actor-isolated 'response' is passed as a 'sending' parameter; Uses in callee may race with later main actor-isolated uses
From which it seems that compiler treats response to be in main-actor region, not in disconnected one. Given that, the only explanation I can have for that is that Task gets treated as having same isolation as response, while completion closure isn't treated to have main actor isolation. This is seems true as modifying the following way removes the error:
await withCheckedContinuation { continuation in
bar.send() { response in
{ @MainActor in
continuation.resume(returning: response)
}()
}
}
Funnily enough, this doesn't work:
await withCheckedContinuation { continuation in
bar.send() { @MainActor response in
continuation.resume(returning: response) // error
}
}