In the previous episode, you learned about the benefits of Swift Concurrency and what problem it solves. Swift Concurrency solves several more problems, which we discuss later in this series. In this episode, we take a peek under the hood to understand how Swift Concurrency does its magic.

The refactored translate(addressString:) method of the Geocoder class looks much better, but it is time to talk about the await keyword and explain how we were able to get rid of the completion handlers. Let's revisit the changes we made to the translate(addressString:) method in the previous episode.

Threads

Like Grand Central Dispatch, you don't need to worry about thread management when adopting Swift Concurrency. Even though threads are managed for you, hiding the nitty-gritty details, we do talk about threads in this episode. To understand how Swift Concurrency does its magic, we need to take a peek under the hood and that means talking about threads. What we cover isn't complicated, though.

Terminology

Before we continue, I want to make sure you understand the terms we use in this series to avoid confusion. In the remainder of this series, I use the terms function and methodinterchangeably. I do that for simplicity. What you need to know is that a method is a function that is associated with a type, a class, a struct, or an enum. Every method is a function, but not every function is a method.

I also talk about awaitable and asynchronous functions and methods. The terms awaitable function and asynchronous function are identical. You may also see the term async function from time to time, referring to the async keyword an awaitable or asycnhronous function is decorated with. With the terminology out of the way, it is time to peek under the hood of Swift Concurrency.

Suspending and Resuming

There are a few important concepts we cover in this episode. The first concept is the suspending and resuming of awaitable functions. An awaitable function, such as the translate(addressString:) method, can be suspended. The async keyword communicates that a function is awaitable and can be suspended. That is the first concept you need to wrap your head around.

This brings up the question what does it mean for an awaitable function to be suspended. Let's take a look at the implementation of the translate(addressString:) method. The geocodeAddressString(_:) method of the CLGeocoder class is, like the translate(addressString:) method, an awaitable function. We use the await keyword to invoke an awaitable function just like we use the try keyword to call a throwing function. The await keyword explicitly indicates at what points an awaitable function can be suspended.

// Geocode Address String
let placemarks = try await geocoder.geocodeAddressString(addressString)

When an asynchronous function suspends, it returns control of the thread it runs on to the system. Don't worry if you don't understand what that means. We break it down step by step. Let's walk through an example to better understand what happens in the translate(addressString:) method.

When the translate(addressString:) method of the Geocoder class is invoked, the guard statement is executed. If the value of addressString is an empty string, the translate(addressString:) method returns control to the calling function by throwing an error. If addressString stores a valid address, execution moves on to the do-catch statement. In the do clause, we encounter the asynchronous method of Core Location's CLGeocoder class. Because the geocodeAddressString(_:) method is awaitable, we call it with the await keyword to indicate a possible suspension point.

The translate(addressString:) method invokes an asynchronous method and that means its execution can be suspended. By invoking an asynchronous method, it returns control of the thread it runs on to the system. The difference with a synchronous function is that an asynchronous function doesn't return control of the thread it runs on to the caller or calling function, it returns control of the thread it runs on to the system. That is what sets a synchronous function apart from an asynchronous function. Don't worry if you're still confused. That's fine at this point.

The moment the translate(addressString:) method invokes an awaitable method, returning control to the system, the thread on which the translate(addressString:) method runs is freed up to take on other work. At a later point in time, the geocodeAddressString(_:) method finishes executing by returning an array of placemarks or, if the geocoding request failed, by throwing an error. When that happens, the system returns control to the translate(addressString:) method so that it can resume its execution.

The translate(addressString:) method executes its remaining statements and returns control to its caller, that is, the function that called it. In this example, the caller is the translate(addressString:) method of the AddLocationViewModel class. Because none of the remaining statements invoke an awaitable function, the translate(addressString:) method finishes its execution without being suspended.

Suspending

It is important to understand that a function can only suspend if it invokes an awaitable function. The system won't randomly suspend an asynchronous function. The await keyword indicates explicitly at what points the function can suspend. Suspension is never implicit or preemptive.

Throwing functions work in a similar way. The try keyword indicates explicitly at what points a function can throw. It isn't possible for a function to unexpectedly throw an error. Only a throwing function can throw an error and you are required to use the try keyword when calling a throwing function. The same is true for awaitable functions.

Let's take another look at the translate(addressString:) method. The await keyword indicates that the method can be suspended when the geocodeAddressString(_:) method of the CLGeocoder instance is invoked. Execution of the translate(addressString:) method is paused or suspended until the geocodeAddressString(_:) method returns an array of placemarks or throws an error.

This is sometimes referred to as yielding the thread. Don't let this term confuse you. It simply means that the system pauses execution of the translate(addrressString:) method, forcing it to relinquish control of the thread it runs on. By pausing execution of the translate(addressString:) method, the thread on which the translate(addressString:) method runs is freed up to take on other work.

DIAGRAM

Suspending over Blocking

Before we continue, I want to make sure you have a good understanding of the difference between blocking a thread and suspending a function. Open AddLocationViewController.swift. Let's take a look at the viewDidLoad() method. As you may know, the viewDidLoad() method is always executed on the main thread since it updates the user interface. Control of the main thread is passed to viewDidLoad() when it is invoked by the UIKit framework. The main thread cannot take on other work until the viewDidLoad() method finishes executing by returning. The viewDidLoad() method is a synchronous method. This implies that it won't be suspended at any point during its execution. It runs from top to bottom, blocking the main thread until it finishes executing by returning.

When the viewDidLoad() method finishes executing, it hands control of the main thread back to its caller, the function that invoked or called the viewDidLoad() method. We say that the main thread is unblocked, ready to take on other work. A synchronous function can only return control of the thread it runs on by returning or by throwing an error.

The same is true for an awaitable or asynchronous function. The translate(addressString:) method gives up control of the thread it runs on by returning an array of locations or, if something goes wrong, by throwing an error. The difference with a synchronous function is that an awaitable function can also give up control of the thread it runs on by suspending. That is a key difference you need to understand.

The moment the translate(addressString:) method invokes the geocodeAddressString(_:) method of the CLGeocoder instance, it returns control of the thread it runs on to the system. This means that the thread can take on other work even though the translate(addressString:) method hasn't finished executing. That is a powerful concept as we see in a moment.

Giving Up Control

The last piece of the puzzle is understanding how synchronous and asynchronous functions give up control of the thread they run on. There is an important difference you need to be aware of.

Open RootViewController.swift and navigate to the viewDidLoad() method. The viewDidLoad() method invokes two methods, the superclass implementation of viewDidLoad() and setupNotificationHandling(). When the UIKit framework invokes the viewDidLoad() method, it passes control of the main thread to the viewDidLoad() method. The viewDidLoad() method is a synchronous method so it blocks the thread it runs on until it finishes executing by returning.

The viewDidLoad() method first invokes the superclass implementation of viewDidLoad(). This means it passes control of the main thread to the superclass implementation of viewDidLoad(). When the viewDidLoad() method of the superclass finishes executing, it returns control of the main thread to the viewDidLoad() method of the RootViewController class. Having control of the main thread, the viewDidLoad() method of the RootViewController class can invoke the setupNotificationHandling() method. It passes control of the main thread to the setupNotificationHandling() method and control of the main thread is passed back to the viewDidLoad() method of the RootViewController class when the setupNotificationHandling() method finishes executing. The viewDidLoad() method has no more statements to execute so it in turn returns control of the main thread to its caller, the function that invoked or the viewDidLoad() method.

What you need to understand is that a synchronous function blocks the thread it runs on from the moment it receives control of the thread until it returns or throws an error. The same is true for awaitable functions. There is one difference, though. There is one other way an awaitable function can give up control of the thread it runs on, by suspending. When a function invokes an awaitable function, it gives up control of the thread it runs on. It doesn't pass control to its caller, though. It passes control to the system. This is one of the most important concepts of this series so take your time to let this sink in.

The awaitable function passes control of the thread it runs on to the system and that has two important implications. First, unlike a synchronous function, an asynchronous function doesn't block the thread when it is suspended. Second, the thread is freed up and the system can use the freed up thread to schedule other work. The system can use the freed up thread to carry out other tasks.

What's Next?

We covered several fundamental concepts of Swift Concurrency in this episode. We use these concepts throughout the remainder of this series so take your time to let them sink in.