Actors 101

Not only, you use actors when you need true parallelism, though of course there's Task.detached too when you don't need to maintain a state.

Now, when you say "protect shared mutable states", protect from what exactly? You don't need protection if you don't have parallelism. And again, parallelism arises from two sources: when you create it yourself (using either of: actors, Task.detached, GCD, or lower-level multithreading calls) or when the OS forces you to execute something on a non-main thread, mainly when dealing with hardware.

If you forget about the OS for a moment and think only in terms of pure actor-based code, here is the funny part: you create actors to protect shared mutable state, but you need protection only when you have actors. Recursion! :slight_smile:

Multiplying actors for actors' sake and just because it's easy in Swift is a dangerous path that I think will lead to a new wave of bloated and inefficient code. You do need to examine your code from the perspective of parallelism and assess the need for actors based on where exactly you need true parallelism and why.

Isn't job the minimal unit of swift currency? A task is composed of multiple jobs, each may run in different isolation domains (that is, actors).

I think both of you are correct. The way how I understand it is that the need for protecting shared state comes from parallelism and actor is a solution to that. In this sense I agree with @crontab's point that one should only use actor when parallelism is needed and @tclementdev's point that most things are in actor rather than being actors.

While it's true that everything is a process in Erlang, I doubt if that paradigm applies to Swift and what's the benefit (it may be different in server application, but in iOS app there aren't that many things that need to be concurrent).

From my understanding the reliability in Erlang is implemented by using OTP library. I don't think Swift has a piece like that. So actor isn't that different from other items in the language in error handing, I think.

BEAM and OTP are just cherry on cake of those ideas. Process is the building block.


Not sure why parallelism appears so often here in discussion, concurrency != parallelism. Big generalisation, but in Swift terms parallel execution is basically TaskGroup, while concurrency is calling different Tasks spread across classes and time.

1 Like

Of course you protect the state only when it is possible to be accessed "in parallel" but the abstraction of Actors leave the burden from the developer detecting if parallelism is possible at any given scenario or not. Actors give you compile time safe synchronization to data, that is how I understood it.
in the past we could have achieved the same thing with dispatch queues and locks but there was no compile time guarantee compared to Actors.

I think the view that clients don't need parallelism is coming from the era of single-core CPU's, but now that you have multiple, there are so many things you could improve in your app.

Here's one example for you: Instagram's former Explore page (now gone I think). You have lots of photos and videos in little tiles, all quickly loading from the internet, but this page should also provide smooth scrolling with absolutely no stuttering regardless of the number of videos being played.

If I remember correctly Instagram developed a library for this purpose and even open-sourced it. Back in the day it was all based on CALayers (not UIViews) and used multithreading extensively. It was quite complex inside.

So what do you do to build a page like Instagram's Explore and make it work smooth? Things that can be done in parallel: download media possibly in parallel chunks, store it on disk, uncompress, display, possibly stream on the fly, scroll (this is done by the system in a separate thread already). For each media file you have 4 stages and for every file you should be able to do it in parallel with everything else while also showing the results on the screen as they arrive. Not an easy task at all once you dive into it and if you want to match Instagram's smoothness and performance. They did an amazing job actually!

1 Like

Nope, parallelism exists not only in task groups. Every actor can execute code in parallel with other actors, also Task.detached implies parallel execution. A single Task.detached already runs in parallel with your current actor.

Concurrency = parallelism, at least in Swift and therefore actors should not be taken lightly.

I also think you give wrong example. TaskGoup doesn't necessarily mean parallelism (for example, suppose your system has only one core).

I understand the difference well (concurrency is illusion, parallelism is reality). The reason I used parallelism, instead of concurrency, was because protecting shared state is usually only required in parallelism.

Neither actors nor Task.detached guarantee parallel execution. You cannot say that it will execute in parallel with other code for sure. You only can say that it will be run in another isolation, on another executor and using another task. This task can end up running even on the same thread.

No matter what language you do use, they are not the same.

Nice talk on the topic:

4 Likes

I didn't say it's always the case, but with enough cores on your system chances of that are pretty high and it means access to shared state should be restricted anyway. You should always assume that both (actors and detached tasks) will be executed in parallel.

Yes, I think I know what you mean but with Swift's model concurrency = parallelism is let's say a safe assumption. The compiler will take care of the details, i.e. that Task { } and Task.detached { } imply slightly different things in their closures in terms of access to shared state, but it's probably easier to think of both of them the same way and rely on the compiler checks for the rest.

You can have concurrency in single-threaded runtime environments (e.g. a coroutine system executing on a single thread).

Yes, I think I understand that. JavaScript and some other mainstream languages are single-threaded but have "concurrency" in the form of just syntactic sugar for completion callbacks. We can agree to call it "concurrency", that's fine, but we are talking about Swift here :slight_smile:

From SE-0296:

This design introduces a coroutine model to Swift. Functions can opt into being async , allowing the programmer to compose complex logic involving asynchronous operations using the normal control-flow mechanisms. The compiler is responsible for translating an asynchronous function into an appropriate set of closures and state machines.

It's the same system.

1 Like

Concurrency is the name of a Swift feature. But in this discussion, concurrency and parallelism are general terms (Swift doesn't have special definitions for these two concepts). So, as explained by many materials on the net, they are not the same thing.

Yes I understand but I'm trying to argue that in Swift and in the grand scheme of things Task { } and Task.detached { } can be viewed as the same thing even despite subtle difference and even though someone would call the former "concurrency" and the latter "parallelism".

In fact the original argument here is around "actors are these cool new thingies that you should use by default" is a result of people taking concurrency as not parallelism. But anyway we are going in circles here, let's see where this "actor by default" is going. (Another OOP-like disaster if you ask me.)

1 Like

The closest to parallelism is TaskGroup, yet even there it is not guaranteed. The only difference between Task and Task.detached is the first one by default inherits priority and actor context, while detached does not. Neither of them can be thought of as a parallel execution.

I think that’s a completely wrong take from statements on higher actors utilization. But let’s go one-by-one:

  1. There is a widely accepted terminology of concurrent and parallel execution, they are not the same, and we rarely write parallel code in day-to-day programming.
  2. Actors not a new thing in CS, they have been around for decades, and stayed there.
  3. Same applies to OOP: we can hate every bit of it and say it produces bad code, but majority of widely adopted languages are object-oriented.
  4. Problem not in OOP as paradigm, but in how it is applied (mostly due to the lack of understanding, probably). Well-written code can be good with any paradigm, and can be trash using the same one.
  5. Finally, and most importantly — nobody here says to use actors by default. It would be insanity to blindly and unreasonably throw actor in every use case. I don’t suggest to use hammer for painting. But as concurrency building blocks they are good. If you don’t use them where they might help, you’ll never know when to use them. We have actors for a few years now, and it is still considered to be something alien and untrustworthy by many. I did the same thing: have avoided actors in majority of the code. Then, when I have had a clear case for actors and tried to apply them, I have failed in design because of lack of understanding.

That’s why I say that since we have actors model in the language, and the model suggests that everything is an actor, when we write concurrent code, we would be better utilizing them more than we think, rather then restrict to a few places. We shouldn’t reach for them first in every situation, but if we question or consider "will actor fit in there?", then give it a shot. You’ll then have much better picture of what actors are, what they good for, and how to use them effectively. No theory would teach that, but theory is an important background, a direction.

1 Like

That's not correct, Task.detached can be executed in parallel with the current actor.

Yes, can – not will. There are a tons of factors that will affect when it will be scheduled for the execution. In a large system you cannot reason if it will be executed in parallel or not, so you have to treat it as a concurrent execution that might be parallel in certain cases.

2 Likes

Where did I say "will"? You literally quoted me saying " Task.detached can be executed in parallel".

You've been arguing from the start that Swift has parallelism, not concurrency, and that Task.detached implies parallel execution.

But I agree with you that this is a pointless conversation, my main take on the topic is here.

FWIW there's an explicit flag called SWIFT_STDLIB_SINGLE_THREADED_CONCURRENCY in the runtime — not sure on what platforms it gets enabled and how (WASM?), but Swift seems to definitively support single-threaded (i.e. not parallel) concurrency.


Overall the distinction of parallel ≠ concurrent is important to actors specifically because actor-isolated functions (or rather, their constituent jobs) will never execute in parallel — and this is what ensures data-race safety — but the functions as a whole may happily execute concurrently — hence the dreaded reentrancy and higher-level race conditions.

3 Likes