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’ve been using Swift Concurrency for the last several months and while it has a lot of nice things going for it there’s still a lot of ways you can “hold it wrong” and get incredibly poor performance. If you block a thread in the thread pool you’re going to deadlock. It’s still pretty easy to create data races (possibly with mis-annotated or poorly thought out types). And logical races are absolutely still a thing, and I might even say they are more common because the messaging is that this’ll magically solve all your concurrency problems and it doesn’t do that.
> If you block a thread in the thread pool you’re going to deadlock.
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.”)
> I’ve found that avoiding deadlocks is as simple as grepping for `DispatchSemaphore|DispatchGroup` in your codebase and eliminating them with extreme prejudice.
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…
Framework code you call into can block so long as it makes forward progress. Deadlocks happen if you lock a thread in the shared Concurrency thread pool, and expect some other code running in the same thread pool to do the unlocking.
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.
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.