Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Follow publication

Swift Concurrency Deep Dive [1] — GCD vs async/await

--

Photo by Bozhin Karaivanov on Unsplash

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

GCD(left) vs. Swift Concurrency(right). From Swift concurrency: Behind the scenes — WWDC21

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

From Swift concurrency: Behind the scenes — WWDC21

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.

From Swift concurrency: Behind the scenes — WWDC21

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.

From Swift concurrency: Behind the scenes — WWDC21

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 threadwhich 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.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in Towards Dev

A publication for sharing projects, ideas, codes, and new theories.

Written by enebin

Making your dreams a reality |  Mainly Interested in iOS | https://github.com/enebin

Responses (2)

Write a response