SE-0316: Global Actors

I added a fourth option to the list above, which I think is also worth discussing. In any case though, I think that the exact fate of Swift global variables is both complicated and separable from the base "Global Actors" proposal here, and should likely be split to its own discussion.

-Chris

1 Like

I feel like synchronous guarantees between @MainActor code are well-covered, since they're identical to synchronous access in the general actor model. What "other code that happens to run on the main executor" are you thinking about? The operation passed to DispatchQueue.main.async? Objective-C code?

We're not doing anything to prevent other instances of the actor, and I'm not sure we need to: only the instance returned by shared is special. One could presumably create some kind of shared implementation that produces a unique actor instance at each call, and we'd lose the guarantee that everything is getting serialized properly, but that doesn't seem like something we should defend against.

You can't add a global actor annotation because you'll break any caller that isn't on that global actor. Removing a global actor annotation is okay in some cases---but not from anywhere that one could infer from. The proposal states a more restrictive requirement because the latter is a bit hard to describe well:

A global actor attribute (such as @MainActor) can neither be added nor removed from an API; either will cause breaking changes for source code that uses the API.

Sure, I'll update the protocol text.

The former. Without this inference, the write to globalTextSize would be ill-formed.

The intention of the proposal is to require this, but it's a point that absolutely needs discussion. I think the issue of unannotated global/static variables is large enough that we should subset it out.

Unclear! The current model of "global" variables in playgrounds and scripts isn't even memory safe without concurrency in the mix, so we need a larger rethink here. If we treat global variables within scripts/playgrounds as if they were locals, there's nothing to do because they fall under the local capture rules.

Unrelated to any of the above, @John_McCall and I were talking about turning the ad hoc protocol for global actors into a formal protocol, e.g.,

protocol GlobalActor {
  associatedtype ActorType: Actor
  static var shared: ActorType { get }
}

It helps formalize the relationship between global actors and actors. We'd then extend the protocol with custom executors to eliminate the "hop" through the actor, e.g.

protocol GlobalActor {
  associatedtype ActorType: Actor
  static var shared: ActorType { get }
  static var sharedUnownedExecutor: UnownedExecutorRef { get }
  // note: we require that shared.unownedExecutor == sharedUnownedExecutor
}

Doug

2 Likes

I think we should use a formal protocol for global actors and suggested it in response to the first pitch [Pitch] Global actors - #6 by anandabits. This would ideally allow code to be written that is generic over global actors:

@MyActor
class MyClass<MyActor: GlobalActor> { ... }

I have a pre-existing library that uses a coarse-grained actor approach where many actor-bound objects run in the context of a single actor. This is encoded in the type system such that objects sharing an actor can be composed while those in the context of different actors cannot. The above generic global actor technique should make a straightforward port of the library to Swift’s actor system possible.

That protocol looks good!

Would we'd provide a default impl for sharedUnownedExecutor right? (shared.unownedExecutor)

Yes, everything which is not marked @MainActor: UIKit callbacks and overridden methods, existing libraries and code bases. I don't quite have a clear idea of the amount of @MainActor annotations that will have to be added, and the amount of interfaces that will have to create suspension points, and potential loss of critical synchronous calls.

1 Like

I have another question. The code shipped in the wild is littered with dispatchPrecondition(condition: .onQueue(.main)) and assert(Thread.isMainThread).

Is there any risk that those assertions are trigged by code that runs on the main actor? Is there a recommendation for developers of libraries whose APIs can talk to both clients who use the main actor, and clients whose code can't use the new runtime (how to rewrite those assertions in a way that is compatible with both environments)?

I am so sorry that I missed/forgot that you had brought this up before! Name lookup for @MyActor looking within the generic parameter list that comes later is a little bit odd, but this seems like a powerful pattern for custom attributes in general and I think it does make sense here.

Yes.

(Objective-)C entities can be marked as being on the main actor with, e.g., __attribute__((swift_attr("@MainActor"))). It's not enforced from the Objective-C side, of course, but it lets C APIs make the promise that they'll always be used from the main thread.

@MainActor code always runs on the main thread/main queue. When you await because you're referencing a @MainActor entity from a non-@MainActor context, it's doing a DispatchQueue.main.async.

Within the confines of this proposal, the only recommendation is "add a new @MainActor API and deprecate the old one". The implementation actually has some affordances for the use case you describe---essentially, it one can state that an API should be on the main actor, but only enforces that property from other code that has adopted Concurrency features (actors, async, whatever). We were planning to bring up those ideas to make it easier to evolve toward concurrency in a separate discussion thread, later, to tackle interoperability between pre-Concurrency and Concurrency code holistically.

Doug

5 Likes

Thank you :+1:

I’m also +1 on the GlobalActor protocol, but wonder if we’d be able to write:

struct T<Actor: GlobalActor> { 
  @Actor a: Int
} 

Other than that, the feature looks very well thought out! I can't wait for it to land in an official release!

Sorry to double-post, but two thoughts on this section:

A closure can be explicitly specified to be isolated to a global actor by providing the attribute prior to the in in the closure specifier, e.g.,

callback = { @MainActor in
  print($0)
}

callback = { @MainActor (i) in 
  print(i)
}
  1. There's no formal grammar specification in the proposal, so it's not clear: if the closure has a capture list, does it come before or after the attribute? (I think I may have brought this up once on Twitter or something; it must have been lost in the shuffle.)

  2. Can we extend this to other attributes that can be applied to closures, in addition to global actors? Result builders are the one that comes to mind immediately, but @Sendable and @escaping might also be useful in niche situations. (Maybe we should sever this from the global actors proposal, though.)

1 Like

Just to follow up on this, I've posted a writeup on how to manage the incremental rollout of Concurrency throughout the ecosystem, which provides some affordances for handling both pre-Concurrency and Concurrency-adopting clients. Let's chat details over there.

Doug

[EDIT: Added the link to the writeup.]

2 Likes

Thanks everybody for your feedback during this review. The core team has returned the proposal for another round of revision.

3 Likes