'child actors' or similar concept

Is there currently a way in Swift to create a sort of 'child actor' relationship where an actor shares the executor of another actor?

I want to rewrite an XML library that I use internally so each element/node is an actor, and when passing the node around, I can call any async function on that node but will be called on the executor of the XML Document actor.

I have been thinking about this for a while and I wonder if it would be beneficial or problematic for something like this.

child actor XMLNode {
       weak var sharedActor: XMLDocumentActor?
       init(sharedActor: XMLDocumentActor) {self. sharedActor = sharedActor}
}

or

actor XMLNodeActor: ChildActor {
       weak var sharedActor: XMLDocumentActor?
       init(sharedActor: XMLDocumentActor) {self. sharedActor = sharedActor}
}

Not currently, but I think actor inheritance is an area the Swift team plans to explore in the future.

However, I think representing XML nodes as actors is probably a bad design. The nodes probably should be Sendable types and the actor the interface/controller to the API or document. Make sure to turn on strict concurrency to see if your design follows good practices for sendable types.

Interesting do you have an example of what you mean. I don't want to miss a finer point that I might be overlooking

The catch here is that the nodes have to be mutable (and not value types, there can be no copying and I need to reference any part of the tree while minimizing lookup over and over again, so reference types it must remain). I don't know how I would accomplish mutable and sendable in a better way that making the nodes actors wouldn't achieve. Actors, after all, are sendable.

I just think it is overkill for all the nodes to have their own executors thus some sort of way for them to share an executor for their 'parent actor' would be interesting to consider.

Value types (including classes with only value types) can be mutable and Sendable. There is no copying as long as there are not multiple references. It is unlikely you would be able to make the nodes actors since actors are not Sendable from another actor.

You need to make sure you are keeping the model separate from the controller. Keep mutations at the controller level to get actor concurrency guarantees. Actors are really designed for the controller layer, not the model layer.

The only other option would be to create a custom global actor. Then your nodes could run controller logic on that global actor from their own classes. I don’t think this is a great design, but this might be the closest to your description. swift-evolution/0316-global-actors.md at main · apple/swift-evolution · GitHub

I think the more philosophical question you need to ask is: how fine-grained do you need your “locking” on the overall document? If you want different threads to be able to simultaneously update different nodes, you’ll need to make each of those nodes an actor. If you’re fine with an update to any part of the document meaning locking out all other threads from doing any work on it, then having the document being the sole executor makes sense (though, it will probably take more effort to implement).

Still, even in the second scenario, I wouldn’t advocate for anything like child actors to solve the problem. You can accomplish what you’re looking for today by making each user-facing node just be a “pointer” into the backing storage controlled by the top-level document (which would be an actor), and any actions on those pointer nodes being async methods that then calls the corresponding actor-isolated action on the backing document.

I'm not sure what you mean by an actor is not sendable form another actor. I was under the impression that sendable types can all be sent across different concurrency domains.

If you are just looking at mutability for UI, I would stick to using the MainActor. You could also put a view model between your XML nodes and the UI.

If you need performance, it is likely going to be much better using mostly value types (or copy on write types) for your data. Actors are not very performant to get data out of since there is locking and contention. They are mainly just a place to keep a minimal amount of state that needs to be centralized and to ensure correctness of accessing that state.

If you’ve never designed anything with actors, I’d look at what others are doing. For example, GitHub - kean/Get: Web API client built using async/await

You can’t really build a graph structure out of actors directly, so anything with an actor per node would be a pretty strange design to work around all the non-sendable type issues.

There are other use cases (many of the cases you would otherwise use locks), but you generally want to think of actors for the controller pattern in a MVC design. That is where your locks are traditionally, so that is where your actors are in modern concurrency. You actor generally just dispatches work that should be mostly composed of Sendable types.

I agree. That seems to be my internal debate. Thank you for your input, I really appreciate it.

1 Like

Just want to address this point, in that this is implementation is fully possible:

actor Node {
    var children: [Node]
}

This would allow each individual node to be modified concurrently, without needing any complex implementation hassles that come with the other solutions (though there’ll still need to be some handling for what happens to a node that is deleted from its parent when another thread is maybe modifying it simultaneously).

Whether this is a good idea performance wise (either theoretically, in the overall Swift Concurrency design, or in practice at this moment) is something I’m not qualified to answer, but it absolutely is possible.

1 Like

Your example might work, but I think it would not be very ergonomic due to await semantics everywhere. There isn’t any way to do actor inheritance, so you would need to use some form of composition instead. I would be concerned about the efficiency in common situations like iteration. You would probably need to use AsyncSequences for all iteration and iteration to children’s children would probably be inefficient.

I would generally make the owner of all the nodes the actor, not the nodes themselves. This would be the simplest solution. Actors were generally designed for the controller layer, although they can replace traditional locks in some situations.

If unwilling to use a controller pattern to access the nodes, a global actor could work. Global actors allow you to put the controller logic anywhere. That is what the MainActor does.

Alternatively you could look at various concurrent container packages that either use lock-free concurrency or traditional locks, but this comes with a cost that may not make sense for many situations. If you are expecting really heavy mutation, there may be better designs like entity component systems (ECS) built on sparse sets. These are common in situations like computer graphics where lots of nodes are frequently updated concurrently.

If you really need to use the proposed design, I think what you want is a global actor. You would basically indicate the model access or mutation need to run in the context of a global actor that owns the model. However, I would consider other ways to architect your controller layer first. Although using a global actor for this isn’t a bad design, there might be a better one depending on what your controller layer looks like.

@globalActor
struct XMLDocument {
  actor DocActor { 
  var root: Node
  }

  static let shared: DocActor = DocActor()
}

@XMLDocument
class Node {
    var children: [Node]
}

Task { @XMLDocument in
    node.children = …
}

1 Like