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
andCoroutine
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.
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.
- In the
main
part, we will rundoSomething
100 times. SincedoSomething
is anasync
method, you need to set a breakpoint withawait
. - Since we want the method to be executed synchronously (sequentially), we will check whether it is accessible using the globally shared
NSLock
. doSomething
is a simple function that pauses a task for 1 second and then executesprint
inside Swift concurrency’s context usingTask
.- When a block in
Task
is finished, thelock
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.