Hi! I've came across an unexpected behavior when explicitly capturing an isolated
parameter in TaskGroup.addTask
closure and I'm trying to figure out whether this is intended behavior or a bug:
actor MyActor {}
func test(isolation: isolated MyActor) async {
isolation.assertIsolated() // OK
await withTaskGroup { group in
isolation.assertIsolated() // OK
await Task {
isolation.assertIsolated() // OK
}.value
group.addTask {
isolation.assertIsolated() // Crash: Incorrect actor executor assumption
}
await group.waitForAll()
}
}
await test(isolation: MyActor())
I've confirmed the difference comes from the @_inheritActorContext
attribute, which is used by Task.init
but not by TaskGroup.addTask
. Nevertheless, since the isolated
parameter is captured explicitly in both cases I would assume that assertions should pass as the closure should become isolated to the capture.
I've made a couple more examples which illustrate this behavior in more detail:
func callWithInheritedActorContext(
@_inheritActorContext closure: @Sendable @isolated(any) () -> Void
) async {
await closure()
}
func test2(isolation: isolated MyActor) async {
await callWithInheritedActorContext {
isolation.assertIsolated() // OK
}
}
func callWithoutInheritedActorContext(
closure: @Sendable @isolated(any) () -> Void
) async {
await closure()
}
func test3(isolation: isolated MyActor) async {
await callWithoutInheritedActorContext {
isolation.assertIsolated() // Crash: Incorrect actor executor assumption
}
}
func test4(isolation: isolated MyActor) async {
let closure: @Sendable @isolated(any) () -> Void = {
isolation.assertIsolated() // Crash: Incorrect actor executor assumption
}
await closure()
}
extension MyActor {
func test5() async {
let closure: @Sendable @isolated(any) () -> Void = {
self.assertIsolated() // Crash: Incorrect actor executor assumption
}
await closure()
}
}
When using sending
instead of @Sendable
or making closures explicitly async
rather than @isolated(any)
the examples crash in the same way.
Recently accepted SE-461 seems to explain that this is not a new behavior:
If the type of the closure is
@Sendable
or if the closure is passed to asending
parameter, the closure is inferred to benonisolated
.The closure is also inferred to be
nonisolated
if the enclosing context has an isolated parameter (includingself
in actor-isolated methods), and the closure does not explicitly capture the isolated parameter. This is done to avoid implicitly capturing values that are invisible to the programmer, because this can lead to reference cycles.
This would indicate that the @Sendable
/sending
closures are unconditionally nonisolated (unless annotated with @_inheritActorContext
as seen above).
However, if this is indeed true and even explicit capture of isolated
parameter should not change isolation of @Sendable
/sending
closures then closures of type like sending @isolated(any)
, used by TaskGroup.addTask
, have rather unfortunate semantics of being nonisolated
and @isolated(any)
at the same time.
I would appreciate if someone could take a look and say whether this is intended behavior. Thanks!