Swift Async and Await
Asynchronous code
Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time. Suspending and resuming code in your program lets it continue to make progress on short-term operations like updating its UI while continuing to work on long-running operations like fetching data over the network or parsing files.
Parallel code
Parallel code means multiple pieces of code run simultaneously — for example, a computer with a four-core processor can run four pieces of code at the same time, with each core carrying out one of the tasks.
What is Async and Await?
An asynchronous function in Swift can give up the thread that it’s running on, which lets another asynchronous function run on that thread while the first function is blocked. When an asynchronous function resumes, Swift doesn’t make any guarantee about which thread that function will run on.
To indicate that a function is asynchronous, you write the async
keyword in its declaration after its parameters
When calling an asynchronous method, execution suspends until that method returns, so you have to write await
in front of the call to mark the possible suspension point. Inside an asynchronous method, the flow of execution is suspended only when you call another asynchronous method which means every possible suspension point is marked with await
.
So Imagine that we need to download a photo, so we will create a function which does the network request and when the data comes back we call the completion closure to notify the caller like this:
func downloadImage(from url: URL, completion: @escaping (UIImage?) -> ()) {
URLSession.shared.dataTask(with: URLRequest(url: url)) { data, response, error in
if let data = data,
let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode <= 300 {
completion(UIImage(data: data))
} else if error != nil {
completion(nil)
}
}.resume()
}
So SE-0296 gave us a super sweet away to handle that by writing async
to the function and use await
to wait to the request to finish and download the photo data.
func downloadImage(from url: URL) async -> UIImage? {
do {
let (data, response) = try await URLSession.shared.data(from: url)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode >= 200 && httpResponse.statusCode <= 300 {
return UIImage(data: data)
}
return nil
} catch {
return nil
}
}
You can see that we just got rid of the completion closure from the first function and added async/await to the second function.
How to use Async/Await to avoid Pyramid of doom
Imagine that we need to execute a sequence of simple asynchronous operations which always requires deeply-nested closures, like imagine that you need to download an image, then resize it, decode it and finally upload it and notify when its done
func processImage(from url: URL, completion: @escaping (Bool) -> ()) {
downloadImage(from: url) { image in
resize(image: image) { resizedImage in
decode(image: image) { decodedImage in
upload(image: decodedImage) { response
completion(response.sccuess)
}
}
}
}
}
So after SE-0296 gave us a super sweet away to handle that we can write the code super clean and neat like this:
func processImage(from url: URL) async -> Bool{
let image = await downloadImage(from: url)
let resizedImage = await resize(image: image)
let decodedImage = await decode(image: resizedImage)
let response = await upload(image: decodedImage)
return response.isSccuess
}
- We started by downloading the image and because downloadImage function is
async
we have to useawait
which suspends execution while it waits for that function to return. - While this code’s execution is suspended, some other concurrent code in the same program runs. For example, maybe a long-running background task continues updating a list of new photo galleries. That code also runs until the next suspension point, marked by await, or until it completes.
- After downloadImage returns, the code will continue execution starting at that point. It assigns the value that was returned to
image
. - The next
await
marks the call to the resize(image:) function and again the code pauses execution until that function returns, giving other concurrent code an opportunity to run. - After resize(image:) returns, the code will continue execution starting at that point. It assigns the value that was returned to
resizedImage
. - and so on until the all the async functions finished their work and finally return the state of the response.
The possible suspension points in your code marked with await
indicate that the current piece of code might pause execution while waiting for the asynchronous function or method to return. Swift suspends the execution of your code on the current thread and runs some other code on that thread instead. Because code with await
needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:
- Code in the body of an asynchronous function, method, or property.
- Code in the static
main()
method of a structure, class, or enumeration that’s marked with@main
. - Code in an unstructured child task.
Calling Asynchronous Functions in Parallel
Calling an asynchronous function with await
runs only one piece of code at a time. While the asynchronous code is running, the caller waits for that code to finish before moving on to run the next line of code.
As in the previous example each function had to wait to the previous function to return its result to continue executing the next function, so what about if you need to run more than one async function in the same time?
To call an asynchronous function and let it run in parallel with code around it, write async
in front of let
when you define a constant, and then write await
each time you use the constant, so imagine that you need to download 4 photos
async let image1 = downloadImage(from: url1)
async let image2 = downloadImage(from: url2)
async let image3 = downloadImage(from: url3)
async let image4 = downloadImage(from: url4)
let images = await [image1, image2, image3, image4]
All 4 calls to downloadImage will start without waiting for the previous one to complete, and none of these function calls are marked with await
because the code doesn’t suspend to wait for the function’s result. Instead, execution continues until the line where images
is defined — at that point, the program needs the results from these asynchronous calls, so you write await
to pause execution until all 4 images finish downloading.
Task Groups
Lets say that we have an array has bunch of urls and we need to download the image for each url, but we need to execute all the network requests in the same time parallel.
SE-0304 gave us Task Group which defines a scope in which one can create new child tasks programmatically. As the child tasks within the task group scope must complete when the scope exits, and will be implicitly cancelled first if the scope exits with a thrown error.
The withTaskGroup
API gives us access to a task group, and controls the lifetime of the child tasks we subsequently add to the group using its addTask()
method. By the time withTaskGroup
finishes executing, we know that all of the subtasks have completed.
func download() async -> [UIImage] {
let urls = [
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
URL(string: "https://images.unsplash.com/photo-1575936123452-b67c3203c357")!,
]
return await withTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
await self.downloadImage(url: url)!
}
}
defer {
group.cancelAll()
}
return await group.reduce(into: [UIImage]()) {
$0.append($1)
}
}
}
We created withTaskGroup
and inside we create a new task for each url to download the image for each url and after than we await for the result of the group to return all the downloaded images in the same time.