[Concurrency] Actors & actor isolation

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.

Yeah, I saw that for async functions. It seemed like Actors were meant to provide a level of serialization above plain async function calls.

For example, this bit:

If we wanted to make a deposit to a given bank account account , we could make a call to a method deposit(amount:), and that call would be placed on the queue. The executor would pull tasks from the queue one-by-one … and would eventually process the deposit.

It's not clear whether "task" above means the Task representing the entire method call, or the PartialAsyncTasks that make up its actual execution. To me, it seems to say that actor method calls would not be interleaved.

Edit: Seeing that the only method in the Actor protocol is enqueue(partialTask: PartialAsyncTask), it probably means the partial tasks.

Yes.

That's not correct. Each async call is potentially a suspension point where other code could be interleaved on the actor. This prevents deadlocks. It's also why we consider it important to mark these in the code with await.

(I think we need to call this out specifically in the proposal)

Doug

3 Likes

I think it's important enough to be repeated on relevant pitches (which would at least be this one & async function). I needed to find that for half of this pitch to even begin to make sense.

3 Likes
Terms of Service

Privacy Policy

Cookie Policy