Calling foo on the below results in two prints for "MainActor". This makes sense to me -- foo is isolated to the @MainActor by virtue of the enclosing context and the closure passed to Task.init inherits that via @_inheritActorContext.
@MainActor final class SomeObject {
func foo() {
print(#isolation as (any Actor)?)
Task {
print(#isolation as (any Actor)?)
}
}
}
What surprised me is that the following code does not print "SomeObject" twice -- it prints "SomeObject" then "nil":
actor SomeObject {
func foo() {
print(#isolation as (any Actor)?)
Task {
print(#isolation as (any Actor)?)
}
}
}
Question: Why does @_inheritActorContext seemingly behave differently if it's a global actor vs an instance actor?
In this particular case, #isolation must resolve to self, but I think because self is being indirectly captured via the macro, it is still defeats the isolation inheritance mechanism of @_inheritActorContext.
I think the semantics of this attribute are incredibly confusing and makes a number of already tricky situations even worse. While there have been a few efforts to investigate improvements here, nothing has come of them so far. But I remain hopeful that one day this is revisited.
Just to clarify, closures formed inside an actor-instance-isolated context must capture a non-aliased reference to the context's isolated parameter (including the implicit self of an actor method) to become isolated to that actor instance. This behavior is separate from the underscored @_inheritActorContextattribute, which restores this implicit isolation behavior for function-type parameters that are @Sendableor sending.
I did forget, as I often do, that this is less about @_inheritActorContext than just closure captures in general. Thanks for the reminder and clarification, @NotTheNHK. But if I'm being honest, every time I re-realize this it makes me feel worse about the whole situation. This is just not intuitive behavior.
For what it's worth I think the team agrees, which is why @_inheritActorContext(always) addresses this and is the default for Task.immediate. I imagine ideally Task {…} would also be updated to have this behavior, but is a breaking change that would need to be reconsidered in a future language mode/version.
I would certainly take just a fix for Task has gotten some traction in the past.
But I really would like to take a closure look at closure behavior more generally, which I don't think can be fixed by a parameter attribute alone. But I've not actually explored changes of that exact nature closely, and I do not know how feasible they would be. And even if they are, you are 100% right that it would be source-breaking, kinda similar to NonisolatedNonsendingByDefault.