Swift Concurrency Deep Dive [2] — Continuation

Enebin
5 min readNov 2, 2022
Photo by Bozhin Karaivanov on Unsplash

This post is written for a deeper understanding of Swift concurrency, that is, async/await.

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.

I highly recommend you to read my previous article, Swift Concurrency Deep Dive [1] — GCD vs async/await

Keyword

  • GCD, full thread context switching, continuation, structured concurrency

What is Continuation?

Concept of continuation

To quote the words of Wikipedia, “a continuation implements (reifies) the program control state, i.e. the continuation is a data structure that represents the computational process at a given point in the process’s execution.”

The following is ‘continuation sandwich’ metaphor introduced in here. It might gives you a quick overview about continuation.

📔 from Here

Say you’re in the kitchen in front of the refrigerator, thinking about a sandwich. You take a continuation right there and stick it in your pocket. Then you get some turkey and bread out of the refrigerator and make yourself a sandwich, which is now sitting on the counter.

You invoke the continuation in your pocket, and you find yourself standing in front of the refrigerator again, thinking about a sandwich. But fortunately, there’s a sandwich on the counter, and all the materials used to make it are gone. So you eat it. :-)

You can think of a sandwich is a part of the data and making a sandwich is what the program does with the data. In this context, continuation is a kind of save point keeping the moment right before making the sandwich.

Bringing back the continuation, you can resume your task from the saved point just before the sandwich was made. The refrigerator is not included in the continuation, so the state about ingredients weren’t saved.

In a nutshell, continuation is an interface that saves the execution state of a program in shared space where it can be recalled from anywhere.

More information

  • It can also be passed directly to another function, which is called continuation-passing. With this, you can pass the current continuation to another function to process the next task inside its context. Programming languages such as Scheme and Coroutine are known to have this feature.
  • There are several ways to implement continuation. Some of them are introduced in this website so check this out if you want.

Continuation in Swift

Swift uses the heap to store state data, that is, continuation used for each suspension point. Continuation is also called async frame in Swift and can be set by the keyword await.

It has very powerful advantages over using only the stack as an non-async method does. Unlike the data stored in the stack, the information stored in the heap can be kept even if the function is stopped or returned.

Let’s dive in little deeper with the native Swift feature (Un)CheckedContinuation which provides a method to create continuation manually.

Using with(Un)CheckedContinuation, you can bring any codes into the context of the Swift concurrency

The purpose of (Un)CheckedContinuation was originally to provide an API that can combine traditional asynchronous codes, closure completion handlers with Swift concurrency.

Completion handler’s not in the context of Swift concurrency, Swift couldn’t know how to deal with its return. That’s why it is necessary to catch and report the moment when getting back from suspended state manually. For that, we can use the instance method resume.

Relationship between thread and continuation

We know Swift stores the execution state of the program(continuation) in the heap. Then why?

Going back to why the heap was needed in the first place, you can see it was because there was necessity for some spaces to be shared across all threads.

Same with this time. Heap-shared continuation means the system can have and provide shared data which can be accessed anywhere and persistent departed from the stack-saved data which always have risk of being empty.

With this, continuation finally can guarantee the reentrancy of the process. All it needs to do is just loading data from memory.

Consequently, it enables to make aforementioned “cost of calling a function” possible and becomes equivalent to saying that “there is no full thread context switching”.

Never block thread. Why?

Even though Swift manages so many things related to concurrency for us, there’re some rules we should stick to. One of them is “never block thread”.

In Swift concurrency, the thread blocking methods we’ve used to manage concurrency, such as NSLock and DispatchSemaphore, cannot be used. The above example is a typical code introduced in WWDC session where a deadlock occurs.

The intention of the above code is as follows.

  1. In the main part, we will run doSomething 100 times. Since doSomething is an async method, you need to set a breakpoint with await.
  2. Since we want the method to be executed synchronously (sequentially), we will check whether it is accessible using the globally shared NSLock.
  3. doSomething is a simple function that pauses a task for 1 second and then executes print inside Swift concurrency’s context using Task.
  4. When a block in Task is finished, the lock will be unlocked so that the next task can access it.

However, as mentioned earlier, the code above causes a deadlock and doesn’t print any logs.

Why? It’s because Task method dispatches block with unstructured concurrency. This requires an understanding of structured concurrency and Task of Swift concurrency. We’ll look in to it in next post.

--

--