I have seen this objection raised many times against Swift's concurrency system. I think it indicates a misunderstanding of what that system is for.
A system of concurrency guaranteeing order is a contradiction. Concurrent means in parallel, which implies no guarantee of order. Guaranteeing order just reintroduces seriality. What are people asking for when they ask for a concurrency system to makes order guarantees?
The answer usually seems to be something about the order tasks are "started", in contrast to full serialization which implies one task is started and finished before the second task is started. First of all, this isn't something new to Swift concurrency. In this code:
Thread.detach { print("Hello 1") }
Thread.detach { print("Hello 2") }
The order of the print statements is indeterminate. That's the whole point of spawning new threads. There's no guarantee that the first thread "starts" before the second thread "starts".
Second of all, "start" isn't even well-defined. Nobody thinks it means the first line in the body (that is, that the print statements will be in order). So what does it mean? There are lots of CPU instructions that get run as part of adding a thread to the kernel's task scheduler and jumping to the function pointer supplied as its main
. Which one exactly counts as "starting" the thread?
So why do people think if you replace Thread.detach
with Task
there ought to be some kind of guarantee about tasks "starting" in a particular order, which is equally ambiguous?
It's the same situation with every other scheduling abstraction. Replace Thread.detach
with DispatchQueue.global().async
and once again, there's no order guarantee (that's the whole point), and it's not even well-defined what it means for one of those blocks to "start".
Regarding re-entrance, why does anyone want or expect actors to continue blocking all message processing after a process yields? The point of the await
keyword is that it means to yield, which enables cooperative multitasking. Because it's not preemptive, it means you don't have to worry about being forced to yield anywhere and have to introduce primitives like locks to deal with that. But two slices of work being inside on async func
isn't any more significant than two critical sections being inside a single func
. Of course there's no expectation that between unlocking then later locking a mutex that no one else locked it in between. That's the whole point of unlocking it!
The reason, I think, is that people don't make an actor
function async
or insert await
s judiciously to yield where appropriate. They insert them because they want to call a function that's async
and not proceed until it completes, so then they have to call await
and then mark the function async
. It's not "this is the right place to put an await
, it's "I put an await
here because I have to". They want and need the whole function to be a single critical section but it just can't be because they happen to need to call an async
function somewhere inside of it.
This is also why people repeatedly ask "can't we just get rid of the await
keyword?" They think it's for the compiler, but it's not. It's for you. To most people async/await
is just "awesome, I don't have to write callbacks anymore!" (the real question there is why so many things used callbacks before instead of just blocking).
This is the language forcing you to abandon poor design. It is equivalent to nested critical sections, a thread acquiring a lock for one piece of state and then while holding that lock, acquiring a lock for an unrelated piece of state, which starves the system because anyone who just needs to access the first state must wait for access to unrelated state somewhere else to finish. Critical sections should be short and to the point, getting their necessary access over with. Nesting them violates this rule. Cooperative multitasking simply doesn't let you do this.
If you start going down this road, what you actually want is a queue. This is a higher level of abstraction built on top of actor
s just like a serial DispatchQueue
is a higher level of abstraction built on top of Thread
s and condition variables.
It might be that some devs get frustrated with Swift concurrency because it's new so we've basically been thrown back to the day where we were just given Thread
and can start doing multitasking. No one's built the NSOperationQueue
of the new concurrency world yet.