[Pitch] Global actors

  • An overridden declaration propagates its global actor attribute (if any) to its overrides mandatorily. Other forms of propagation do not apply to overrides. It is an error if a declaration with a global actor attribute overrides a declaration without an attribute.

I assume this means that overridden declarations from a non-isolated superclass are not actor-isolated to the global actor, which would mean that in the following code, viewDidAppear would not be actor-isolated:

@MainActor
class IconViewController: UIViewController {
  var url: URL?

  override func viewDidAppear() {
    // Error: cannot access actor-isolated property 'url'
    // from non-isolated context
    self.url = URL(string: "https://example.org")!
  }
}

This seems annoying, since viewDidAppear is always going to be called on the main thread (as per the UIKit/AppKit framework). Is there any mechanism proposed whereby ObjC code can declare a type as actor-isolated?


The pitch also showed an example of an ObjC-exposed global-actor-tagged property:

class IconViewController: UIViewController {
  @MainActor @objc private dynamic var icons: [[String: Any]] = []
  // ...
}

This won't be exposed to ObjC because it's private. But if it were not private, what would such a declaration look like in ObjC? Is the actor-isolation preserved in any way when exposed to ObjC? If so, how? If not, does that expose a potentially source of unsafety, where a caller might access a @MainActor property from a background thread in ObjC? Maybe that unsafety is acceptable, similar to how ObjC code can pass nil to a non-optional parameter, but I think it would be useful to define the proposed behavior.

A lot of correctness of all these concurrency proposals in the UI world relies on the fact that UI frameworks will have to annotate their promise or expectation that "we guarantee these are called on the main actor".

You're right though the pitch here is not explaining how that would happen. We had discussed a form of @MainActor(unsafe) or @MainActor(unchecked) (or any @GlobalActor(unsafe)). This version would allow to express the guarantee that this will only be called correctly, and if it wouldn't it would crash. It would be up to frameworks to annotate their APIs with such "guarantees".

I'm not sure what the exact plan with annotating existing APIs is, but there exists at least the API notes mechanism to do just that, add additional semantic information in obj-c headers for the swift compiler to take into account during import. I don't know if that would be used or if the plan is to use some other technique though :slight_smile:

1 Like

Did you consider including a refining protocol?

protocol GlobalActor: Actor {
    var shared: Self { get }
}

I have a library that encodes concurrency context in the type system and limits some functionality to objects (each managing a piece of state) that share the same context. Expressing this in the actor system using the above protocol would look like:

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

As you can see, this would require the protocol as well as the ability to "see" the generic context of a type in attribute declarations. As we expand the use of user-defined attributes I can imagine the latter capability being useful for other things as well.

2 Likes

Yes, one can use __attribute__((swift_attr("@MainActor"))) to state that an Objective-C declaration will always be called on the main actor. Of course, we can't verify that from the Objective-C side, but it makes annotation possible. This is documented in SE-0297, albeit somewhat indirectly because we hadn't written up the global-actors pitch fully at that time.

Ignoring the private, the declaration in Objective-C could use the attribute I mentioned above. And yes, it's completely unsafe from the Objective-C side--we've had to live with that in Swift since day 1, where (Objective-)C code can freely violate the safety guarantees otherwise provided by Swift, and I think it's okay to do the same thing here.

I'll bring some discussion of this into the proposal, thank you!

Doug

3 Likes

Can a global actor also have a public initializer?

In other words, it would have a shared instance, but it wouldn't be a singleton.


Here are some alternatives to the shared property:

  1. Use the same name as the attribute (or protocol).
    public static var globalActor: Self { get }

  2. Use the name suggested by Dave Abrahams et al.
    public static var singleton: Self { get }

  3. Use a static subscript without parameters.
    public static subscript() -> Self { get }

Hmm. Let me see if I understand this correctly:

  1. I can magically convert a class hierarchy to being a (single) actor by applying a global actor to the base class. (Edit: Not correct, as each actor instance gets its own execution context. So the result would not be an actor, but "act" like one in most cases.)
  2. I can make a bunch of actors use the same concurrency domain / actor context by not making them actors (using "actor XYZ") but instead making them classes in a global actor (using "@myActor class XYZ"). (Edit: Sort of. They are sharing the same concurrency domain, but they are not really actors, as that requires separate concurrency domains for all instances.)
  3. I can create an actor by first creating a global actor, and then doing "@myActor class XYZ" instead of "actor XYZ". So you could perhaps argue that "actor XYZ" is syntactic sugar for "@tempPrivateActor class XYZ"... (Edit: No, the result would not be an actor, see above.)

This makes a lot of sense to me. But I think it makes the separation between "actor XYZ" and "class XYZ", and the whole discussion about whether actors should support inheritance, a bit odd... :smiley:

(Edit: Corrected after reading @xwu's response.)

2 Likes

I agree with @BigSur. I prefer "global actor".

This is incorrect. Consider the example of actor BankAccount. Each instance of a BankAccount protects its state from every other instance of a BankAccount. A similar class would not with a global actor applied.

2 Likes

It was implied that each tempPrivateActor would be different.

By what mechanism would each class instance get its own global actor type?

1 Like

So this is just sort of a game to understand what is really going on.

If we imagined that we wanted to in fact implement actors this way, the compiler would have to create a new global actor type for each such class-turned-into-an-actor. Like e.g. __tempPrivateActor_1234 or something like that :grin:

My point is whether this super nice feature of global actors requires an implementation that actually ends up implementing actors in a very simple way.

The compiler/language could also create a "public" global actor with a more reasonable name, so it was easy to make a set of actors share the same execution context...

I think you’re missing the point of what’s going on. An actor protects its own state. If a global actor could be instantiated uniquely for each instance, then it would cease to be global, meaning it would be an...actor. But that still doesn’t make a class in a global actor itself an actor, it would still be...a class in an actor. That is because there is nothing about the class that protects its own state. If you’re asking if you understand correctly, though, that a global actor is an actor, but global, then yes.

1 Like

Ohh. Thanks, i noticed the word "instance" now :grin:

So what you are saying is that I did not understand this correctly, and that a class with a global actor applied is not like an actor. This is because each actor instance will get its own execution context, where a class with a global actor applied will share that actor amongst all instances :+1:

Thanks for enlightening me :+1: :slight_smile:

But it still stands, I think, that by implementing this feature we almost get an implementation of actual actors for free. So I would speculate that it could make sense to not make actors behave too differently from classes...

Hi all, i'm sorry for the delay getting to this.

From what I can tell, there are a few different things going on here:

  1. There is the ability to declare an actor as a global actor, which prevents instances from being freely created. Because there is only one instance of the thing floating around we get nice properties we can reason about it.

  2. there is a tag that can be put on global var declarations (and presumably static vars as well), associating those globals with the global actor. This provides a form of (extremely important) syntactic sugar to say that these things are semantically instance variables of the global variable, and get the same isolation rules as an ivar.

  3. there is a tag that can be applied to a wide range of other declarations that mark those declarations as being "part" of the actor.

Taking each of these things in parts, here is my quick opinion about the shape of this:

  1. Details aside, I think that we need the ability to define global singleton actors like this, and the syntax proposed is reasonable. It is a nice to eliminate the let shared boilerplate if possible, particularly if there is a specific semantic form required - why not synthesize that in the compiler? In any case, this is a nit that can be discussed, not a big issue.

  2. I am pretty concerned about discussing global variable tags without a larger discussion of global variable semantics in the face of swift concurrency. This is a major hole in the memory safety model and affects both structured concurrency and actors. We need a unified model for how to deal with this. Once we have that unified model then of course it makes sense to have a way to tag globals as being "part of this global actor", but it is imperative that it be consistent with the bigger model, and I think we should discuss that model first.

  3. the general tagging thing proposed is a bit concerning to me: these tags are going to go everywhere and significant expand the type system. All that complexity (which is definitely type system exposed, and will also be user exposed in common cases) is really only about controlling access to mutable global state by type tagging the world that could touch it. Such a thing has been discussed before (in the "can we have a 'pure function' effect" threads) and I think that all the concerns about that apply here too.

Since this whole thing centers on "what our approach is for global variables", I think it makes sense to discuss that. If we land on a simple and scalable approach that doesn't require tagging, then we may not need pervasive effect marking. I also think the of the @sync feature for referring to actors is directly related. Instead of being a parameter attribute, perhaps there is a way to merge it with the tagging above. Such a thing is definitely in the space of the tagging thing, but would apply to all actors, not just global ones.

-Chris

3 Likes

I expect the introduction of new global actors to be very rare, so introducing syntactic sugar doesn't seem to be all that important.

With the main thread, the global state isn't your own mutable global state you have access to, it's... everywhere. Certain operations need to be executed only on the main thread. You can't see the state they operate on because there is a library boundary there. You can't call them "pure" because they aren't. The reason I called out "just do the main actor for now and don't generalize the feature" is because tagging is unavoidable for the main actor---the main thread is too pervasive.

If we find a solution for global variables that isn't tagging them with global actors, that's fine. It doesn't obviate the need for @MainActor, and if we need to decouple the discussions, I'm also fine with that.

Doug

Fair enough.

Sure, I think I confused the issue - I realize that "main thread" and "pure" are different concepts. I meant that the effect tagging (and the ramifications it has on language, library, and composition thereof) is very similar.

-Chris

Yes, it's another effect system. The good news is that we know how to integrate these; the bad news is that we've already added two of them as part of concurrency (async and @Sendable).

Doug

1 Like

Yes async adds a new effect for sure. Definitions may vary, but I don’t personally consider @Sendable to be an effect. Sendable checking is just type checking of values through protocol conformance checks, it doesn’t have the same nature and transitive infection properties as effect systems (which apply to function like decls, not to types). Similarly, “mutable” is not an effect, it is a type qualifier for self with special syntax because the self decl is implicit.

When comparing the proposed global actor effect system thing to the two existing effect systems (throws and async) in Swift, you can see that this would be a novel/different/unusual beast:

  • the existing effects all have marking (await/try) but this would not.

  • due to marking, the existing effects allow local reasoning about why they are required (and thus have a more predictable api evolution impact), whereas this one models a more nonlocal “calls something that touches a global actor global variable” property

  • the existing effects are sugar for (among other things) a type wrapper on the result of the function (implying a hidden Result or Future type) but this is not

  • the existing effects impact code generation, but this one does not

  • the existing effects were introduced when the corresponding language features were introduced, but this is trying to retroactively improve type checking for a design pattern that goes back to NeXT days. Impact: this will break tons of existing code, instead of allowing progressive adoption over time.

While I can see some attraction of building invasive type system support for modeling this sort of stuff, I don’t see how it is particularly related to the other parts of the swift concurrency model. I think huge progress would be made by making this sort of legacy code memory safe by /dynamically/ detecting that a tagged global is accessed from the wrong context/thread, and forgo static checking, type system extensions, and the massive api auditing, annotation, and api evolution problems that such things would bring with it.

-Chris

1 Like

Perhaps a compiler flag allowing to opt out of global actors (or just MainActor) could be an option? I think most projects would really prefer doing the work to move to MainActor. It just seems to be such a huge improvement to me.

I'm currently trying to fix main thread access of some central properties in my project. Without the help of a static analysis tool it's an enormous task. I cannot just add asserts as I cannot be sure to actually exercise all code paths. I for one would really like compile-time checked MainActors :grinning_face_with_smiling_eyes: