Why not just make all mutable types actors?

My team and I are facing the long slog of purging an 2MLOC codebase of Swift 6 issues. We're looking beyond the migration to best practices & patterns.

One approach we're exploring is making all class types into actors unless there's a strong need to support inheritance. I think there are only two downsides to this approach:

  • the type cannot be used from legacy synchronous nonisolated code
  • a tiny but non-zero performance hit for each call into the type to decide if the calling task needs to suspend.

AFAIK, that performance hit is too small to be worth considering in all but the most compute-intensive use cases (e.g. a game engine).

What other downsides might we be missing?

One thing to remember is that actors are re-entrant. If you had a collection of collaborating classes you converted to actors, you have to await every call between them. And because each actor is their own isolation domain, every call will hop to that actor. It's possible for new calls to come in from another task into the graph of actors and now you have to make sure actors are all consistent in that case.

That may be the right call for your codebase. But in my experience I found it better to think about what pieces need to ratchet together. Consider making an actor that coordinates all of that. In fact you can keep some pieces classes and even make them non-sendable but owned by an actor. Because they would be non-sendable then the only way to access those pieces is through the actor where they are all kept consistent together.

I'm assuming a lot about your situation though. Ultimately I do think it would be better to think of actors as subsystems that need to keep themselves consistent instead of just "converting all classes to actors".

1 Like

You generally use class types because there's a whole graph of objects that reference each other. It may be sensible to protect the entire graph with an actor — this is, essentially, what the main actor does with the view / view controller tree on Apple platforms — but making every individual object into its own independent actor is likely to just lead to headaches and bugs in the common case that you need to make consistent updates across objects.

5 Likes

Yeah if I were in the position of having a bunch of classes and a willingness to redesign the code (and to be clear, "every function call between all my classes is suddenly async" IS redesigning the codebase), I would start by looking to see if most of them could be structs, rather than actors.

The stdlib effectively went down this road long ago, iirc there's only two a few public reference types in the entire library, and they're quite niche.

3 Likes

Right - the compiler is already pointing out that we have whole swaths of code written before concurrency came out that will need some level of rethinking. I like the idea of pushing for structs first and see what dead ends emerge.

1 Like

Another thing to consider is what your existing concurrency safety story was. If it was "well, we kinda hoped for the best" then you have a lot of work ahead, but you also are likely going to see mysterious issues disappear. But, if it was something more coherent than that, there may be a way of translating it into something more directly usable without a complete redesign.

For example if your strategy was "the classes have a lock internally that they use to protect their state", you can generally just mark them Sendable rather than jumping through a lot of hoops.

Similarly, if it was "they're not safe but they'll only ever be called from one thread, so it's fine", that might be a situation for a global actor annotation (possibly even @MainActor), rather than making each individual one an actor.

2 Likes