While trying something I found the following code didn't compile:
@MainActor
func test1() async {
var a = 1
await perform {
a = 0 // note: sending main actor-isolated value of non-Sendable type '() -> ()' to nonisolated global function 'perform' risks causing races in between main actor-isolated and nonisolated uses
}
}
@concurrent
func perform(_ fn: () -> Void) async {
fn()
}
This surprised me because I had thought compiler was able to determine that a and the closure capturing it are in the same disconnected region and transfer them to global executor. This is how it works when there is no closure involved.
Example: it works when there is no closure involved
Compiler has no problem in transfering the disconnected region containing ns1 and NS2(ns1) to global executor:
class NS1 {}
class NS2 {
let ns1: NS1
init(_ ns1: NS1) {
self.ns1 = ns1
}
}
@MainActor
func test() async {
let ns1 = NS1()
await send(NS2(ns1))
}
@concurrent
func send(_ ns2: NS2) async {}
I found a workaround:
@concurrent
- func perform(_ fn: () -> Void) async {
+ func perform(_ fn: sending () -> Void) async {
fn()
}
But IMO the workaround shouldn't be required, because sending parameter is intented to affect callee (perform), instead of caller (test).
Then I realized this was caused by closure isolation inference rules.
- When
perform(_:)'s closure parameter isn't markedsending, rule 1 in SE-0461 takes effective and the closure is inferred MainActor-isolated, hence the error. - When it's marked
sending, rule 2 in SE-0461 takes effective and the closure is inferred nonisolated, hence no issue.
Hmm...I didn't expect there would still be such simple code I didn't fully understand.