Speaking of the concurrency manifesto, I keep going back and looking at its stated aims and thinking about whether they have actually achieved them: —
- Design: Swift should provide (just) enough language and library support for programmers to know what to reach for when a concurrent abstractions are needed. There should be a structured "right" way to achieve most tasks.
- Maintenance: The use of those abstractions should make Swift code easier to reason about. For example, it is often difficult to know what data is protected by which GCD queue and what the invariants are for a heap based data structure.
- Safety: Swift's current model provides no help for race conditions, deadlock and other concurrency problems. Completion handlers can get called on a surprising queue. These issues should be improved, and we would like to get to a "safe by default" programming model.
- Scalability: Particularly in server applications, it is desirable to have hundreds of thousands of tasks that are active at a time (e.g. one for every active client of the server).
- Performance: As a stretch goal, it would be great to improve performance, e.g. by reducing the number of synchronization operations performed, and perhaps even reducing the need for atomic accesses on many ARC operations. The compiler should be aided by knowing how and where data can cross task boundaries.
- Excellence: More abstractly, we should look to the concurrency models provided by other languages and frameworks, and draw together the best ideas from wherever we can get them, aiming to be better overall than any competitor.
That said, it is absolutely essential that any new model coexists with existing concurrency constructs and existing APIs. We cannot build a conceptually beautiful new world without also building a pathway to get existing apps into it.
— from Swift Concurrency Manifesto · GitHub
Let's take those one at a time:
Design: Swift should provide (just) enough language and library support for programmers to know what to reach for when a concurrent abstractions are needed. There should be a structured "right" way to achieve most tasks.
Is that true? Say I want to avoid synchronization problems in my code: I still have a choice of a global actor, a local actor, one of the new semaphore/mutex tools they've released, the old locks eg OSAllocatedUnfairLock
, and so on. And I don't think it is clear what is best to use, especially within an existing codebase where adopting actors can be very hard — because you can't always easily introduced concurrent bits of the code into existing deep code paths. And it changes what needs to be Sendable
and what doesn't too. The tradeoffs are complicated.
Maintenance: The use of those abstractions should make Swift code easier to reason about. For example, it is often difficult to know what data is protected by which GCD queue and what the invariants are for a heap based data structure.
I think the existence of this thread in the first place puts doubt on this one. Actors, Sendable, sending, crossing actor boundaries, the difference between isolation context, isolation domain, isolation region, and so on. These are not simple things to reason about — especially as sometimes they only exist as compile time concepts unlike GCD queues.
Plus invariants are still not simply described anyway. Compare with say, MPE where you have a directive to mark a section of code directly as a critical section. In Swift that kind of thing is covered by a (probably global?) actor where the actor function itself is synchronous. That is not clarity and could easily be broken when a future change requires that code to become async.
Safety: Swift's current model provides no help for race conditions, deadlock and other concurrency problems. Completion handlers can get called on a surprising queue. These issues should be improved, and we would like to get to a "safe by default" programming model.
Data races may be eliminated — if and only if everything is compiled with swift 6 and trusted of course. But general race conditions remain and you get no help with those.
Thread deadlocks may technically not exist but logical flows can still deadlock easily — one clear example is using withContinuation
and then failing to call the continuation. Swift can runtime detect and warn about that sometimes but not always.
And reentrancy is alive and well — instead of completion handlers getting called on a surprising queue, now any code after an await can be called on a surprising thread. If you adopt the concurrency model completely and properly, then sure maybe it doesn't matter what thread you are on. But that is not always possible and bugs from actor reentrancy do exist.
"Safety" as a concept in all these discussions is interesting. It is a certain, technical meaning of safety that is not necessarily actual safety. Memory safety is definitely a good goal to have, but it is not synonymous at all with the kind of help they are implying here. Take MainActor.assumeIsolated
as an example — this is safe by the technical definition because no undefined bahaviour can happen because we crash the application instead. Personally, I don't think of my application as being able to crash as "safe". Another example is the opt-in task cancellation. You now can't guarantee that some task won't live forever and deadlock your app, because it has to allow itself to be cancelled cooperatively. That is literally less help with deadlocks than we had before, not more. (see withThrowingTaskGroup doesn't (re)throw the error - #11 by rurza)
Scalability: Particularly in server applications, it is desirable to have hundreds of thousands of tasks that are active at a time (e.g. one for every active client of the server).
Performance: As a stretch goal, it would be great to improve performance, e.g. by reducing the number of synchronization operations performed, and perhaps even reducing the need for atomic accesses on many ARC operations. The compiler should be aided by knowing how and where data can cross task boundaries.
These goals are the ones that have the best claim to actually having been achieved. How many Swift developers were actually suffering from these problems though?
Excellence: More abstractly, we should look to the concurrency models provided by other languages and frameworks, and draw together the best ideas from wherever we can get them, aiming to be better overall than any competitor.
We could argue about this but personally I don't think they are anywhere near being able to claim they are better than any other competitor.
That said, it is absolutely essential that any new model coexists with existing concurrency constructs and existing APIs. We cannot build a conceptually beautiful new world without also building a pathway to get existing apps into it.
We only need to look at this thread again and see they failed at this. Eg: —
I deeply resent the hours-upon-hours wasted working around concurrency challenges with Apple’s own preconcurrency frameworks. (I’d love to see research into the total loss of productivity across the industry that all of this has introduced! It makes me think wistfully about the Swift 2 to Swift 3 naming convention transition, and that was a PITA.)
I feel like the victim of a “move fast and break things” philosophy, which is all fine and dandy if you’re the one “moving fast”, but is far less fun for those of us living with these “broken things”.
The old way and the new way are so estranged they had to introduce new mutexes and locks etc because the new way wasn't good enough in all cases.
So in conclusion the goals of the original manifesto can only really score 2/7 at this point. And given the route they have taken, it seemed they valued the performance and scalability over all the other points. If there wasn't a limited thread pool, a lot of these other issues would be easier. They constantly reference Erlang in the manifesto as support for the actor model, but Erlang actors do not really resemble Swift actors at all, and the thread pool is one of the reasons that is so. Unfortunately it is far too late to backtrack on that now and I don't know how one would start going about fixing it. There has been an emphasis on documentation in this thread, but I would argue that few to none of the problems I listed above would be solved by documentation. Even the concept of a "clear right way to do something" — it's not clear if you have to read a book first.