[Pitch #3] Actors

Hello again, everyone,

After yet more interesting discussion in pitch #2, we've revised the actor proposal for this, pitch #3.

Changes in the third pitch:

  • Narrow the proposal down to only support re-entrant actors. Capture several potential non-reentrant designs in the Alternatives Considered as possible future extensions.
  • Replaced @actorIndependent attribute with a nonisolated modifier, which follows the approach of nonmutating and ties in better with the "actor isolation" terminology (thank you to Xiaodi Wu for the suggestion).
  • Replaced "queue" terminology with the more traditional "mailbox" terminology, to try to help alleviate confusion with Dispatch queues.
  • Introduced "cross-actor reference" terminology and the requirement that cross-actor references always traffic in ConcurrentValue types. This goes along with the latest pitch on ConcurrentValue.
  • Reference @concurrent function types from their separate proposal (which his the same proposal as ConcurrentValue).
  • Moved Objective-C interoperability into its own section.
  • Clarify the "class-like" behaviors of actor types, such as satisfying an AnyObject conformance.

Doug

23 Likes

Wow, this is huge progress. All of the changes are great!

I have some further suggestions on how to improve the type system modeling of actors, which I posted in this thread. Here are some comments from a read through of draft #3, which does not take into consideration that direction:


Minor typos:

  • "Implementation note: At an implementation level, the messages are partial tasks (described by the [Structured Concurrency] proposal)". -> it seems like Structured Concurrency should be a link out.
  • Broken punctuation: "However, the [Alternatives Considered](#alternatives-considered] section".

I don't understand this description in the closures section of the proposal:

The first bullet is always true, and I think you describe this earlier in the closure section. I think it is enough to say that @concurrent functions are never actor isolated.

I don't really understand the second bullet though. It would help my understanding to be more specific about what is tied to the callee declaration, the function type the closure is inferred to, and the closure itself. How much of this is clang importer rules (e.g. implicitly importing completion handlers as @concurrent) vs rules for working with other random Swift 5 code?


I think this is just a writing thing, but in:

It would be good to add some rationale here. There is nothing memory unsafe (AFAIK) about this, it is just likely to trip up memory exclusivity checks and is an unnecessary booby trap to allow this.

If this is required for correctness, then the restriction would have to be consistently applied to local variables as well as stored properties, since they are actor isolated state as well. I think that this is actually ok though because you get diference instances for these things for each stack frame, which makes them ok in practice. It would be good to capture some of this in the writing.


You explained the interaction between actors and ConcurrentValue really nicely in the Cross-actor references and ConcurrentValue types section!


In detailed design, "An actor may only inherit from another actor. " --> I thought inheritance from NSObject was allowed?


It's not clear to me how nonisolated(unsafe) works: it looks like it can be applied to stored properties, which means that the property can be accessed from nonisolated methods. However, can it also be applied to methods as well? Are nonisolated(unsafe) properties only accessible from nonisolated(unsafe) members? Does unsafety propagate out or no?

Reading later it looks like methods can be declared as nonisolated(unsafe) which allows them to do things to arbitrary isolated state. This seems overly dangerous to me, I think it would be better to require all the data members to be "opted into" unsafety, rather than having extensions be able to throw arbitrary unsafe logic onto actors.

I'm not super excited about the whole nonisolated and nonisolated(unsafe) thing in general, if it remains, then I think it is important to get the safety story right on the (unsafe) version otherwise it can rapidly undermine safety goals of the entire model.


In the isolation checking section, the term "executable code" is a bit confusing to me (this isn't a .exe file). Is there a better term that can be used here?


The Protocol Conformance section has an offhand mention that protocol requirements can be marked nonisolated. If this is intended, then it would be good to explain the full model here. What do they mean with structs and classes that conform to the protocol, what are the subtyping rules, what does "nonisolated(unsafe)" mean on a requirement, etc.


In Partial Applications it would be good to spell out the type of self.synchronous explicitly, e.g. something like this:

  let fp1 : () -> () = self.synchronous                        // seems ok
  let fp2 : @concurrent () -> () = self.synchronous  // invalid conversion 

It would also be useful to expand out those two sentences at the end of this subsection into examples, because I don't understand exactly what you mean.


I'm very happy see the discussion of non-reentrancy included in the proposal, and I'm also very very happy to see it is in the future work section.

Overall, really great job on this!

-Chris

1 Like

I like how nonisolated is spelled now.


The reference to other.accountNumber is allowed based on this rule, because accountNumber is declared via a let and has value-semantic type Int.

Shouldn't that be "and has ConcurrentValue-conforming type Int"?


However, the [Alternatives Considered](#alternatives-considered] section

(markdown syntax error)


Perhaps let properties deserve a mention within the "Non-isolated declarations" section in " Detailed design". Right now it's mentioning mutable stored properties but there's nothing about stored properties that aren't mutable. I would expect it to say that let variables are non-isolated when they conform to ConcurrentValue, or something like that.


We could introduce a @reentrant attribute may be added to any actor-isolated function

Possible redundancy in italicized parts may be redundant.

I believe this is about exclusivity, which seems it would be trivial to trigger violations of in face of actor re-entrance.


The nonisolated(unsafe) is just a simple "turn off any checks", nothing more. The nonisolated actually still does some checks, but the unsafe one is unsafe.

So for a property it means it can be accessed by anyone, without any checks; For a function it means it can access anything (isolated, nonisolated, nonisolated(unsafe)), without any checks.

It is equally unsafe as your proposed withUnsafeActor... {} alternative, so the overly dangerous claim I don't think really sticks. It is the "I know what I'm doing" escape hatch that either approach has a need for. That's what unsafety is, and I don't think properties need to opt-into "i can be unsafely accessed". They could opt into "I can be safely, concurrently acessed" which in this proposal is spelled as nonisolated.

Either shape to achieve this is unsafe by design and there needs to be some backdoor to implement those things, be it nonisolated(unsafe) or withUnsafeActor...{}.


Thanks, and I hope to be able to PoC this soon... It is pretty nerve-wracking to not have this crucial piece of what actors are supposed to be doing locked in, (IMHO: prevent races), especially as the discussion about the defaults is quite crucial here. Hopefully will manage to PoC and discuss deeper soon though :crossed_fingers:

An actor is a form of class that protects access to its mutable state

Actors behave like classes in most respects

Is an actor a special kind of class, or is an actor similar to but distinct/separate from a class?

What is the practical distinction in your mind?

I had meant to read this iteration of the pitch at least twice before commenting, but time constraints being what they are, better to be a little more timely and a little less thorough, I think.

Thanks for the shout-out on the nonisolated spelling. I'm glad others feel the same about this. Regarding implicitly inheriting this from protocol requirements and superclasses, I feel that it is somewhat precedented for @attributes, but now that it's spelled nonisolated, it would be nice to align with the rules for nonmutating, etc.: which I think would require declarations in subclasses and protocol-conforming types to restate it explicitly. I would argue that this is justifiable just as taking off the @ is justifiable in the first place: it is an important enough distinction to be cognizant of that we should have it visible.

I found your analysis over in the other thread about type system implications to be persuasive. Both you and @Chris_Lattner3 do raise questions as to whether nonisolated(unsafe) carries its own weight or could appropriate be split into nonisolated at the declaration and an internal with... { } function; I do agree that the latter would separate user-facing concerns from implementation details more explicitly.

I have to admit that I'm lost on the status and progress of the discussion on @concurrent implying @escaping (or was it vice versa?) -- it would be nice to have that in one place about the design options in this space and the pros and cons of each; not sure if that's best done here or in the ConcurrentValue pitch.

Apologies for the scattered feedback :)

1 Like

I haven't fully thought it through, but there is probably not much practical distinction. I was just wondering how I should think about it in my mental model.

Several clarification requests:

  • Is super is considered self access?
  • Assuming callable value is supported in actors, is implicit "call as function" call exactly the same as explicit callAsFunction method call?
  • Is keypath supported on actors? If so, is the isolation rule for [keyPath:] subscript the same as normal subscript?
  • Similarly, how about dynamic member lookup?

What is the idea for unsolicited up-calls in the new actor world?

Like a way to specify a set of signals on an actor and a mechanism to connect to those signals from another actor so that handlers will be called on the receiving actors executor.

Yes, it is. I'll clarify it for the next round.

Yes, this is a syntactic transform.

Same.

Hmm, good point. We cannot form these safely. Thank you!

Doug

2 Likes

nonmutating doesn't work in classes, but the same issue crops up with protocols, e.g.,

protocol P {
  var p: Int {
    get
    nonmutating set
  }
}

struct D: P {
  var p: Int {
    get { 6 }
    set { } // error: require you to state nonmutating here
  }
}

I see your point here, that nonisolated is important enough to the semantics of your declaration that it should be called out explicitly. OTOH, I wonder how much boilerplate we're causing with this rule. I'm going to fret about it a bit more before changing anything.

Doug

1 Like

It's not limited to the Clang importer, as existing Swift code is equally as likely to take a completion handler (or other escaping function parameter) that is then called later, concurrently with the actor.

I'm clarifying and expanding on this section somewhat.

Right, it's not memory-unsafe, but reentrancy of actors means that it is very, very easy to trigger new kinds of exclusivity violations. I'm adding more rationale.

This was a mistake: nonisolated(unsafe) should only apply only to stored properties, and should mean "don't do isolation checking." Really, I'd like to replace nonisolated(unsafe) with @concurrent(unsafe) down in the ConcurrentValue proposal, because it's a general "opt this declaration out of concurrency checking" annotation that shouldn't be specific to actors.

I agree that "executable code" is strange. I'll talk about this in terms of calls and references instead.

Ahh... this is somewhat tied in with global actors, which we're separating out. I'll simplify this down to eliminate the notion of actor isolation for requirements here.

This section is a mess. I'm going to rework it a bit.

Thanks for the review!

Doug

Sorry, I didn't mean to suggest that nonmutating works in classes; I elided several other keywords into the "etc.," most pertinently Swift's rules requiring (hah) the restatement of required upon overriding required initializers in a subclass.

Hey all,

We've revised the actors pitch again based on feedback here, thank you! There's a new thread for pitch #4.

Doug

2 Likes