SE-0316: Global Actors

Hi everyone. The review of SE-0316: Global Actors begins now and runs through June 7th, 2021.

This review is part of the large concurrency feature, which we are reviewing in several parts. While we've tried to make it independent of other concurrency proposals that have not yet been reviewed, it may have some dependencies that we've failed to eliminate. Please do your best to review it on its own merits, while still understanding its relationship to the larger feature. You may also want to raise interactions with previous already-accepted proposals – that might lead to opening up a separate thread of further discussion to keep the review focused.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. If you do email me directly, please put "SE-0316" somewhere in the subject line.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at:

https://github.com/apple/swift-evolution/blob/master/process.md

Thank you for participating in improving Swift!

Joe Groff
Review Manager

11 Likes

In propertyWrapper chaining scenario, if different PW has different isolated-actor either globalActor or instance actor, which one will be the running executor when accessing pw/$pw ?

@someGlobalActor
@propertyWrapper
struct PW1 {...}

@propertyWrapper
actor PW2 {...}

struct S //default global concurrent executor
{
@PW1 @PW2 @... var pw=wrappedValue...
}

Is there actor/executor hopping when accessing await pw / $pw? Perhaps would hit potential performance issue switching between different globalActor/actors.

It seems the definition of global actor hasn't changed from last pitch:

@globalActor
public struct MainActor {
  public static let shared = /* unspecified actor type */
}

I think the author may forgot to update?

2 Likes

Thanks for catching that. I will correct the proposal text.

+1

Is there a way that nonisolated could be modeled as a global default actor that is implicit in pre actor Swift code? A default global actor for non annotated code could also address the static variables issue. Has something like this been explored ?

Thanks,
Chéyo

Under the “Closures” section, is there preexisting syntax for this, or is there a typo using $i instead of i?

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

That's a typo.

It's a typo, thank you! Fixed here.

nonisolated should mean that it's accessible from everywhere, synchronously, so we cannot put global variables there by default. A global/static let of Sendable type could be accessed synchronously from within the module (in line with the recent amendment to SE-0306).

Mutable state cannot be nonisolated at all, unless we invent something like the nonisolated(unsafe) that was part of earlier revisions of SE-0313.

Now, we could consider putting mutable global/static variables on a specific global actor. For example, we could say that unannotated global/static variables are implicitly @MainActor, and perhaps even say that top-level code is on the main actor to reduce friction there.

Doug

1 Like

A class can only be annotated with a global actor if … the superclass is NSObject .

This wording seems to preclude the @MainActor class IconViewController: UIViewController example shown earlier; UIViewController is a subclass of NSObject but the wording here suggests that the superclass must be exactly NSObject. Or are we assuming that UIViewController will be imported with a @MainActor attribute?

(Either way, I hope the IconViewController example is allowed, because it’s a compelling use case.)

I'm very excited about this. Am I correct in understanding that this could potentially remove the need for a lot of DispatchQueue.main.async when interacting with UI elements/state?

In any case, +1.

That's the idea, yes :slight_smile: Rather than having to interact with queues and make sure to be "on the right one", you'd annotate your functions, properties and types with @MainActor to get execution on the main actor be ensured automatically by the compiler and runtime.

1 Like

Sounds like ObjC wouldn't be able to participate in this so in effect, most code that needs to run on the main thread (UIKit) would be excluded from having this safety? Also it sounds like if you annotate something as MainActor and it can't be guaranteed that the calling code is on the main thread (ie called from another MainActor context) then you have to call it using async code. Wouldn't this lead to the case where a UIKit callback like viewDidLoad or a button's action has to then create a task to call anything marked as MainActor?

1 Like

I'm not sure how I should ask my question.

Often, when you develop UIKit apps, you need both to make sure some code runs on the main queue, and to avoid a queue hop (DispatchQueue.main.async { ... }) if the caller is already running on the main queue.

Really?

Yes! People do check for Thread.isMain, or for the main DispatchQueue (with specifics), in order to preserve a synchronous behavior when possible!

Come on, this thing is so subtle, and so important, that RxSwift has three ways to specify how you want your code to be dispatched on the main queue:

  • MainScheduler.asyncInstance: always after a queue hop
  • MainScheduler.instance: always after a queue hop, unless the caller is already on the main queue, and the scheduler is not called in a reentrant way.
  • ConcurrentMainScheduler.instance: : always after a queue hop, unless the caller is already on the main queue.

Talk about a crucial feature!


Guarantees of synchronous calls are critical in order to avoid unwanted visual glitches.

Let's give an example. Say you want to push a view controller that displays information about a book. It is crucial that the book is available before the push animation starts, or you may see a flash of an empty screen right in the middle of the push animation. This flash is the sign of an unwanted DispatchQueue.main.async { ... }.

You'll ask: "but why don't you just load your book before pushing your screen?"

I'll answer: this muddles responsibilities. Sometimes you really want to encapsulate the book screen and its data source / view model / store / whatever as a unit that is fully responsible for loading and displaying the needed data. So you do not want the previous screen, the one from which you navigate to the book screen, to be responsible for getting a book value before pushing the book screen. Instead, you want the book screen to be autonomous, and so you need to make sure it is able to grab the book synchronously, in order to avoid the visual glitches I described above.

The consequence is that it would be really not good at all if undesired dispatch queue hops could happen when two @MainActor functions / classes / closures call one each other, or when undecorated code that happens to run on the main queue calls a @MainActor function / class / closure.

To me, this is the difference between a tool that is gorgeous and does what I need, and a tool that stabs me in the back after I've spent hours and days refactoring my code for the new shiny fad. There is nothing worse than discovering that the tool can not do what I need (it's not that it is difficult, it is that it is impossible, because the tool does not understand what people need).

Please remember that UI programming sure needs guarantees to run on the main queue, but it also needs a lot of synchronous code.

Is this topic addressed in the proposal? Am I alone feeling somewhat nervous?

2 Likes

I believe that at least the first part of your concern here is addressed in the proposal (emphasis added):

Any declaration can state that it is actor-isolated to that particular global actor by naming the global actor type as an attribute, at which point all of the normal actor-isolation restrictions come into play: the declaration can only be synchronously accessed from another declaration on the same global actor, but can be asynchronously accessed from elsewhere.

Calls/accesses between two declarations isolated to the same global actor are synchronous—there is no potential suspension.

1 Like

Can we read a "can" as a "must"?

I wonder if synchronous calls, in the spirit of the proposal, are more than an optimization.

I'm not after an optimization, but a reliable feature.

And it's OK if global actors don't provide synchronous guarantees. I just prefer to know it upfront, so that:

  • I can avoid them when they can't address my needs
  • I can report a feature request to the language authors.

:-)

The async/await model here guarantees synchronous access from what I understand. If I'm able to call funcOnTheMainActor() without an await, then there's no potential suspension point and so the call is guaranteed to be synchronous.

The "can" here, I believe, should be read as a "can" for the user, rather than the language runtime. Synchronous access (or not) is something visible in the source, and therefore something the user has control over. If you're not writing await, the access is synchronous. The global actors model enforces rules such that attempts to write a synchronous access to a global-actor-isolated declaration from a different global actor (or none at all) is a compile-time error. But from the same global actor, users "can" write a synchronous call and have it compile. (Of course, users "can" also write an asynchronous access to the declaration as Task.detach { await funcOnTheMainActor() }, so "must" doesn't seem appropriate to me.)

I see what you mean. Yes, this looks like a guarantee indeed, thank you! :+1:

Maybe the proposal authors will add extra information, around the fact that we also need synchronous guarantees in the interaction between @MainActor code, and other code that happens to run on the main executor.

Sometimes you want to know if a problem is fully acknowledged by the proposal authors, or a mere detail lost in a sea of indirect consequences.

2 Likes

To add to this - I believe this is covered in the 'reentrancy' section of the main Actors proposal. Suspension can only happen if there is an await in the code. Swift Actors are re-entrant, and therefore have the same issue (phrased in the proposal as possibly breaking invariants). The invariant in UI code is whether the run loop has a chance to spin and start a new Core Animation transaction.

I have a couple of detailed questions below, but I am overall +1 on this proposal.

That said, there is a caveat on this - the proposal isn't clear what happens with unannotated global variables, and the decisions made here have a profound impact on the language, user model, and migration story for Swift Concurrency. There are two ways to handle this: 1) pick a design and try to wrestle this to the ground in this proposal, or 2) land the general design for Global Actors in this proposal, then drive a new "Swift Concurrency meets Global Variables" proposal as a follow-on.

Beyond that major issue, I also have a serious concern about the inference rule for closures (mentioned below). Overall though, this is very nice work. Some train of thought notes below:


The approach of using a user-defined attribute is very nice. I also agree that @MainActor is a natural name for this thing, aligning with the precedent set by the @main attribute. The ability to mark both global vars and functions is very nice.


The approach described in Defining Global Actors is very sensible. What is done to prevent creating other instances of the actor? I think we need to prevent this somehow (for memory safety), perhaps with a dynamic check?


In Using global actors on functions and data, it makes total sense to be able to mark methods as pinned onto an actor. How does this interact with API resilience/evolution?

Can protocol requirements be marked as @MyGlobalActor? (later: yes apparently according to the inference section). It would be great to mention this earlier in the proposal.


Global actor function types: nice. The subtyping rules look great.


In Closures, I'm not sure I understand the last bit:

Are you saying that the @MainActor aspect is inferred from the type of callback or are you saying that it is inferred from the type of globalTextSize which happens to be uttered inside the closure?

The former is super sensible to me, the later is very scary and I think it is unprecedented. The consequence of that is that changing the body of the closure will change the inferred type of the closure, which could lead to downstream changes.

The only thing I'm aware of close to this is inference of the throwing and async bit on the closure. However, that inference is done syntactically and can be done before name lookup. This is a different beast.


Global and static variables

I'm +1 on allowing globals to be tagged, this is the core part of the model. However, I'm not sure what is being proposed:

The phrasing "we can require" makes it sound like a decision hasn't been made here, and this is a key part of the design that has to be nailed down.

I think there are some non-controversial cases to cover:

  1. Explicitly annotated globals are obviously ok and tied to a global.
  2. Following the precedent of the actor proposal, we should allow @Sendable lets to be usable within the module they are defined in.
  3. We could extend #2 to @frozen or some other attribute for cross-module let's.

The question is, what do do about unannotated vars, public lets that are used-cross module, etc. I see a few different options with different tradeoffs, all of which provide memory safety:

  1. Reject them as a compile time error. This will have massive source breakage and isn't a very friendly model.
  2. Codegen their accessors to produce a runtime error when accessed from a Task (either structured or actor), but allow access from pthreads an other legacy code. This is source compatible and provides progressive migration path, but is a footgun. However, this will be a much easier to detect and fix problem than optional unwraps and array out of bound errors that cause similar traps.
  3. We could codegen their accessors into implicitly async accessors, sort of treating each global variable as if it is on an anonymous global actor. This would also be a major source break (because you would only be able to touch these in async functions) but would provide nice model over time.
  4. We can codegen globals to be uninherited unstructured task local values, which don't require @Sendability. This would fill in a hole in the design space but more importantly, would provide full source compatibility for Swift concurrency. They do have somewhat new semantics that we'd have to teach, but I think the benefits outweigh the costs.

Coming back to the major point though, I don't know what the proposal is proposing here :-).


What is the proposals model for playgrounds and scripts? We obviously shouldn't have boilerplate annotations like @MainActor on every top level variable. How does this work / how does the proposal handle this?


At first blush, the inference rules look ok. However, I didn't really think deeply on them and consider all the angles. The meta question I'd think about is: how does this work with resilience and API evolution? Is this all consistent with what we do there?


In Source Compatibility, this claim is made:

Global actors are an additive feature that have no impact on existing source code.

This is true of the core feature, but the handling of global variables is complicated and is almost certain to be source incompatible in some ways. This is the core issue in this proposal.

-Chris

6 Likes
Terms of Service

Privacy Policy

Cookie Policy