Swift Concurrency Deep Dive [1] — GCD vs async/await
This post is written for a deeper understanding of Swift concurrency, that is,
async/await
.
I usually get information for iOS from WWDC sessions but sometimes it seems that quite many stuffs are not covered enough in the sessions. This time, while studying Swift concurrency, I came across similar kind of challenges again and decided to gather helpful information in one place for future reference. That’ why I started writing this post 😎.
I’ve collected information from reliable sources like Apple Developer’s Document, Swift-evolution repository or Swift Language Guide as much as possible, but may contain incorrect information. In that case, please let me know in the comments.
Keyword
- GCD, full thread context switching, forward progress, continuation
Swift Concurrency as an alternative for GCD


In several WWDC sessions, Apple has shown its intention to replace GCD with Swift Concurrency. It was very evident in the WWDC session like Swift concurrency: Behind the scenes — WWDC21 or Meet async/await in Swift — WWDC21. According to those, errors that GCD can possibly generate are like as follows.
Flaw of GCD 1: Vulnerable to thread explosion
Thread explosion mainly occurs when dispatching long-running methods in a concurrent queue. The following is an example of the code that causes the problem.
The code above assumes that each loop adds a task which consumes more than 5 seconds to the queue. This could be tasks such as loading large files or downloading data from the network.
Since the queue’s not serial and does not wait for the return of the previous block with sync
method when it dispatches, the OS will try to process as many tasks as possible simultaneously. As a result, the OS makes the following decision:
// Logs from console0 <NSThread: 0x600003938000>{number = 5, name = (null)}
5 <NSThread: 0x60000392d380>{number = 6, name = (null)}
6 <NSThread: 0x60000392c540>{number = 7, name = (null)}
2 <NSThread: 0x60000392cbc0>{number = 8, name = (null)}...95 <NSThread: 0x600003938540>{number = 42, name = (null)}
98 <NSThread: 0x60000392c740>{number = 50, name = (null)}
97 <NSThread: 0x600003920740>{number = 46, name = (null)}
99 <NSThread: 0x60000392c800>{number = 25, name = (null)}
Creates as many threads as possible!
If that number exceeds 100 and even increases toward like 1000, it’s clear that the system will crash.
Flaw of GCD 2: Overhead from frequent context switching

If you’re lucky, OS might stop creating more threads right before the system crashes. In this case, however, the overall performance can be degraded, even if the system does not crash.
This is because the more threads there are, the more frequent full-thread context switching occurs in CPU. It is a well-known fact that how much full-thread context switch wastes the resources of system.
Can Swift concurrency solve those problems?
The answer is yes.

If so, how could Swift concurrency solve the problems?
It’s because Swift concurrency is designed to reuse idle threads instead of blocking them.
Also, from there, tasks processed by Swift concurrency do not necessarily belong to a specific thread as before. In other words, it means when a task gets back to the thread to process the remaining operations, the thread may not be the thread that originally started the task.

It means OS now can allocate tasks to any idle threads to execute them.
In GCD, a suspended(sync
) task should wait for return of the currently running task. However, Swift concurrency just drops that task from the thread and assigns another task waiting for execution to fill the empty space.
Therefore it becomes possible to maintain the minimum number of idle threads, and as a result, one step closer to “1 core, 1 thread” which is the goal of Swift concurrency.
💡 from WWDC Session
This means that we now only pay the cost of a function call instead. So the runtime behavior that we want for Swift concurrency is to create only as many threads as there are CPU cores, and for threads to be able to cheaply and efficiently switch between work items when they are blocked.
However, it seems to repeat the second flaw of GCD, “the frequent context switching problem”.
Actually, the Swift concurrency is free from this because full thread context switching doesn’t happen in here. To quote the WWDC session, it costs only “the cost of calling a function” for Swift to exchange tasks, and consequently the cost of context switching is vanished.
You can easily figure it out with a simple experiment. The following is a few lines of code that prints 100 times as in the previous example of GCD.
It’s a group of tasks that execute a print statement with 1 second delay. try await Task.sleep(nanoseconds: 1_000_000_000)
means to suspend for 10⁹ nanoseconds, that is, 1 second at the corresponding suspension point.
TaskGroup
is a group of dynamically configurable tasks. TaskGroup
allows you to run child tasks in parallel and receive a callback on completion after all running child tasks have completed.
The execution result of the above statement is as follows.
0 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
1 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
3 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
2 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}...96 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
99 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
98 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
92 <NSThread: 0x6000011e88c0>{number = 7, name = (null)}
Compared to the example of GCD above, all operations were performed on the same thread while they were executed in parallel.
There’s another thing you should care about. Here, if you use sleep(1)
like GCD, you’ll see that each task is executed serially as if using the sync
function. This is because sleep
is a method that blocks a thread.
Apple defines an action that blocks a thread like sleep
as a non-forward progress and doesn’t recommend using it in the context of Swift concurrency.
It’s because blocking threads arbitrarily can cause problems such as deadlock, which can have an unexpected break down of the system. Other examples can be NSLock
or DispatchSemaphore
.
We’ve looked into how Swift concurrency solves two main flaw of GCD, vulnerable to thread explosion and overhead from frequent context switching.
So, now you might be wondering about what kind of magic made all this possible. This is because the Swift concurrency implemented the continuation.