Global actors vs. Task vs. MainActor

The Xcode 13.4RC release notes contain the following improvement:

  • Closures that are guaranteed to run on the main actor are now permitted to reference local variables from their enclosing scopes that are also running on the main actor.
@MainActor
class MyController: UIViewController {
  var isExcited: Bool = false

  func doSomething() {
    var title = "Hello"

    Task { @MainActor in
      if isExcited { // okay, accessing main actor-isolated stored property
        title += "!" // okay, accessing main actor-isolated local variable
      }
    }
  }
}

(90665432)

That this change seems limited to the MainActor seems odd to me given the original language of SE-0304.

Context inheritance

Unstructured tasks created with the Task initializer inherit important metadata information from the context in which it is created, including priority, task-local values, and actor isolation.

If called from the context of an existing task:
...

  • if executed within the scope of a specific actor function:
    • inherit the actor's execution context and run the task on its executor, rather than the global concurrent one,
    • the closure passed to Task {} becomes actor-isolated to that actor, allowing access to the actor-isolated state, including mutable properties and non-sendable values.

Perhaps I've misunderstood this section this entire time, but I originally thought it meant Task {} inherited the actor context it was created in, including global actors. But I'm not sure. Does this only apply to calls from within actual actor types? Or does it only apply to Tasks within Tasks within actors?

In short, can someone clarify the actual rules around Task actor inheritance and explain how the change in Xcode 13.4 violates it for MainActor?

5 Likes

I am not sure what you mean by “…inherited the actor context it was created in, including global actors”. That sounds to me like you think actor-isolated code can run in the context of more than one actor. But that is not the case, as I understand it: I remember reading that actor-isolated code is associated with and can access variables from only one actor.

As I understand it, a closure can capture an actor-isolated local variable, and within the closure, the captured variable maintains the same actor association. If the closure is not is running on that same actor and accesses that variable, it has to do so with async. But if the closure is on the same actor, it does not need the async.

So my interpretation of that release note is that there was a bug involving a redeclaration of the same actor context being treated as a different actor, but now it works just like it always should have been expected to.