-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Description
Description
See this example. It compiles on both 6.2 and nightly build. It looks like passing the nonisolated(nonsending) closure fn to bar, which takes a @concurrent closure, causes data race.
foois called in multiple Tasks.- In each call of
foo, the closure is actually run on global executor and hence accessA.nssimultaneously.
Perhaps compiler shouldn't convert a nonisolated(nonsending) closure to @concurrent if the closure captures value that is in region other than disconected region or task-isolated region?
class NS {}
actor A {
let ns = NS()
func foo() async {
let fn: nonisolated(nonsending) () async -> Void = { _ = self.ns }
await bar(fn)
}
func bar(_ fn: @concurrent () async -> Void) async {
await fn()
}
func test() {
Task {
await foo()
}
Task {
await foo()
}
}
}
Note it appears that the data race couldn't be observed in practice, but the result is still weird. See more details in reproduction section.
Reproduction
Below is a complete test program that I used to demonstrate the data race. However, Its output isn't what I expected. fn isn't excuted on global executor, instead it's actor isolated.
task1 | BEGIN fn [output.A]
task2 | BEGIN fn [output.A]
task2 | END fn
task1 | END fn
A few comments:
- Based on the output there isn't data race.
- However, the output doesn't make sense. While it's true that
fninfoois actor isolated, I wonder how it's converted to a@concurrentclosure when passed tobar? IMO the convertion perhaps should fail in the first place. It looks like what happens is that the convertion succeeds but the result is still an actor-isolated closure, instead of a@concurrentclosure? I think it's worth to investigate what really happens.
class NS {
var value = 0
}
func currentIsolation(isolation actor: (any Actor)? = #isolation) -> String {
return "\(actor, default: "non-isolated")"
}
func logStart(_ fnName: String, _ isolation: String) {
print("\(Task.name ?? "") | BEGIN \(fnName) [\(isolation)]")
}
func logEnd(_ fnName: String) {
print("\(Task.name ?? "") | END \(fnName)")
}
actor A {
let ns = NS()
func foo() async {
let fn: nonisolated(nonsending) () async -> Void = {
logStart("fn", currentIsolation())
_ = self.ns
try? await Task.sleep(for: .seconds(1))
logEnd("fn")
}
await bar(fn)
}
func bar(_ fn: @concurrent () async -> Void) async {
await fn()
}
func test() {
Task(name: "task1") {
await foo()
}
Task(name: "task2") {
await foo()
}
}
}
let a = A()
await a.test()
try? await Task.sleep(for: .seconds(3))
Expected behavior
The code shouldn't compile
Environment
It can be reproduced on 6.2 and nightly.
Additional information
No response