[Concurrency] Actors & actor isolation

Enqueuing a partial task is the primitive, low-level operation that we want all executors to provide. The partial task fully encapsulates the unit of work that needs to be scheduled without any added overhead. Wrapping that up as an ordinary first-class function value would introduce a lot of overhead: partial tasks are one-shot and self-consuming, function values are not. It would also be semantically problematic because a call to an async function always happens as part of a task, but an executor itself is not a task; the async function would have to ignore its given context and introduce the appropriate task context.

5 Likes

Ideally, I would like global actors to be global (singleton) instances of an implicit corresponding class, so that you wrote something like:

public global actor UIActor {
  func enqueue(...) {}
}

and the identifier UIActor would (in most contexts) resolve as a reference to the singleton instance of the UIActor class. I think that is much cleaner. It is also, however, a whole feature in and of itself.

5 Likes

PartialAsyncTask needs to stay as is for performance reason, got it.

What about execute being protocol requirement, and not separated extension, and other non-final shenanigan? It feels like this is more appropriate to be in an extension:

extension Actor {
  func execute(...)

or even global function:

func execute(partialTask: ..., using actor: ...)

And run seems like it should be overridable:

protocol Actor {
  func run(...)
}

enqueue needs to be the customization point, or else the general enqueue has to forward to something.

The details of turning the function passed to run into a partial task and enqueuing it are not actually interesting to customize in an executor implementation.

2 Likes

I'm pretty sure we do need another value here for "actor-confined"; withoutActuallyEscaping(_:do:)'s documentation explicitly discusses using it to run non-escaping closures concurrently, and changing our minds about that being legal seems tantamount to a source break.

5 Likes

Copying a discussion from the roadmap thread per suggestion from @Lantua:

If these accesses are disallowed, from the "First Phase: Basic Actor Isolation" example in the roadmap thread:

    // error: an actor cannot access another's mutable state
    otherActor.mutableArray += ["not allowed"]

    // error: either reading or writing
    print(other.mutableArray.first)

What is the meaning of mutableArray being declared internal?

I see how this proposal adds an axis beyond access control—mutableArray is restricted even beyond what private would signify:

synchronous functions may only be invoked by the specific actor instance itself, and not even by any other instance of the same actor class.

(my emphasis)

What I am wondering is if these axes are orthogonal: is it at all meaningful that mutableArray is internal here? I'm not sure that access control modifiers really matter at all for actor state. Perhaps they would matter if the state was annotated @actorIndependent? I wonder if the language or the developer tools might clarify this at all.

At the very least, access control modifiers will affect the visibility of declarations from extensions, which are actor-isolated.

2 Likes

AFAICT, public actor-dependent and private actor-independent both make sense:

actor class X {
  public var dependent: ...
  @actorIndependent private var independent: ...
}

/// Same File
func foo(x: X) {
  x.dependent // error: actor isolation
  x.independent // ok
}

/// Separate module
extension X {
  func bar() {
    x.dependent // ok
    x.independent // error: access control
  }
}

Looks pretty orthogonal to me.

4 Likes

If these accesses are disallowed, from the "First Phase: Basic Actor Isolation" example:

    // error: an actor cannot access another's mutable state
    otherActor.mutableArray += ["not allowed"]

    // error: either reading or writing
    print(other.mutableArray.first)

What is the meaning of MyActor having declared access to mutableArray to be internal? Is mutable actor state automatically considered private?

I see how this is a separate axis, yes, and how actor state is restricted even beyond what private would imply. From the link you share:

synchronous functions may only be invoked by the specific actor instance itself, and not even by any other instance of the same actor class.

(my emphasis)

What I am wondering is if these axes are orthogonal: is it at all meaningful that mutableArray is internal here? I'm not sure that access control modifiers really matter at all for actor state. Perhaps they would matter if the state was annotated @actorIndependent? I wonder if the language or the developer tools might clarify this at all.

Maybe we can jump over to the discussion thread? I don't think this fits the roadmap thread.

(This was moved from the roadmap thread upon request, the question was answered a few posts above).

2 Likes

Something about the name actor class feels weird to me. Will there ever be actor structs or actor enums? If not, and actors will always be classes, why mention it at every definition?

I mean, the proposal says:

Actor classes behave like classes in most respects: the can inherit (from other actor classes), have methods, properties, and subscripts. They can be extended and conform to protocols, be generic, and be used with generics.

But then goes on to show a bunch of examples which would be valid in a class but not in an actor class. It's worth noting that methods, properties and subscripts are in no way unique to classes, nor are extensions, protocols or generics.

The only thing actors can do which is in any way class-like is support inheritance. To be honest, inheritance is often a massive pain (especially when considering things like subclasses and Equatable), so most of my actors will be final. I wouldn't even mind if they didn't support inheritance.

I see actors as a fundamentally new thing. They enforce data isolation in a way that is wholly unique in the language, and if anything is more like the grouped-exclusivity rules of value types than the very liberal rules which apply to stored properties of classes.

9 Likes

They're also reference types, and therefore have reference identity. They satisfy AnyObject constraints. We call them actor class because they are a restricted form of class, and nearly every intuition one has about classes also applies for actor classes.

Doug

7 Likes

Doesn’t this pretty-much just boil down to reference types with reference identity, though? The other things, like concurrent access to stored properties, don’t apply.

For me, I’d prefer a shorter syntax, since AIUI this is the fundamental unit of data synchronisation. Data within an actor is always synchronised with respect to the other data members, and if you want one piece to live on its own timeline you’d encapsulate it in its own actor.

5 Likes

I've been trying to figure this out. If I have an old custom executor, say DispatchQueue or some kind of RunLoop, and would like to wrap it as a (global) actor, how should I go about doing it? There don't seem to be a direct way to call async function from inside sync function, and old executor would accept only sync ones. Feels like it would need to open up PartialAsyncTask somehow.

I think that's a totally reasonable interpretation (and undoubtedly how a not-insignificant portion of the Swift community uses the terms), but we should make sure we're being consistent in how we're using this terminology (i.e., fix TSPL if we want to change how these terms are expected to be used). Also, if "value type" and "type with value semantics" are synonymous, do we need a general term for "type defined by a struct/enum/tuple"?

IIRC I've been corrected myself on this usage by @dabrahams, who may have stronger feelings than I about the terminology here. In any case, I've opened a PR to reword the Escaping reference types section in terms of "semantics." If that doesn't feel as though it more precisely communicates the intended meaning, feel free to decline!

2 Likes

PartialAsyncTask will have some kind of synchronous run() operation that should allow one to do this. For DispatchQueue, we will probably want to add some API to allow you to run an async operation on that particular queue. The details here will evolve as more of the pieces of the prototype implementation come together.

Doug

1 Like

It doesn't sound that much different from converting it to first class function (including the call-once restriction) which seems to be contrary to what @John_McCall said earlier (quote below). Or do you plan to have some fast path for default execute implementation?

Regarding this (code) comment, from the Actor Isolation section (near the end):

// Safe: this operation is the only one that has access to the actor's local
// state right now, and there have not been any suspension points between
// the place where we checked for sufficient funds and here.

Does this mean that, if one actor method call suspends (because it calls an async function), then other method calls on that same actor could run while the original is suspended? That is, could separate method calls on an actor be interleaved?

From the rest of the proposal, I would have expected that a given actor method call would run completely before any others were allowed to run.

Yes, it's in the Async Function pitch.

This design currently provides no way to prevent the current context from interleaving code while an asynchronous function is waiting for an operation in a different context. This omission is intentional: allowing for the prevention of interleaving is inherently prone to deadlock.