I think the main issue with GCD is the potential for deadlocks in serial queues (which it doesn’t really help you with), and the related problem of thread explosion in concurrent queues.
Actors make protecting shared mutable state really easy, solving a big chunk of the reason you’d want to use semaphores and serial queues in the first place. If you stick with async/await/Task{}/actors, you’re guaranteed to only have the optimum number of threads that can saturate cores, and won’t have deadlocks. If you use Sendable properly and heed all the warnings, you’re a good way towards the kind of “fearless concurrency” that Rust gives you. It really is pretty great IMO compared to GCD.
I’m actually happy with this decision… it was very much done intentionally to prevent the kind of thread explosion that is all too common in GCD. The criticism of GCD I linked to was very skeptical of actors for this reason; if the implementation decides to add more threads to avoid deadlocks, you get the same performance pitfalls as in GCD, and thankfully that didn’t happen.
In practice, I’ve found that avoiding deadlocks is as simple as grepping for `DispatchSemaphore|DispatchGroup` in your codebase and eliminating them with extreme prejudice. If you stick to Tasks/TaskGroups/Task.sleep, there’s no real possibility of accidentally introducing deadlocks unless you try really hard to do so.
And yeah, logical races can still happen in actors, but it’s easy enough at a glance to tell whether you’ll run into one… if you avoid using `await` in a section of code that needs to run exclusively, you’ll be fine. (Even with `await` calls you may get lucky if there’s no actual suspend point happening, but a quick and dirty rule is just “don’t call await and your code will never be run concurrently.”)
You’d think so but then you realize that framework code you call into might decide to do this and you wouldn’t know until it deadlocked. On a 6-core iPhone 13 this is probably not going to be noticeable, but that’s probably not true on a 2-core iPhone 6s…
But if you’re calling into framework code which is using GCD, the queues it dispatches into will be run on a separate GCD thread pool. If said framework code is using a semaphore or lock to block your calling (Concurrency) thread, then it stands to reason there’s another thread in the GCD pool which will eventually unlock it. (Or else it’s a deadlock no matter what you do.)
I haven’t come across any framework code which violates this, although maybe you’ve run into problems I haven’t.
That said, I'm really not sold on actors. I think CS as a whole hasn't really landed on the right abstraction for concurrency yet, actors seem a bit experimental, and GCD is plenty good enough in most cases.