The async/await feature has been added as part of the newer, more structured take of Swift with concurrency and was released with Swift 5.5 during WWDC 2021.^1 Concurrency is the ability to execute multiple computations asynchronously. Swift added two keywords: marking asynchronous functions with async
, then calling them using await
.
Problems and Motivations
Before the addition of async/await, asynchronous programming was achieved with completion handlers. Completion handlers was the de facto code block to send back values after executing a function. Writing one is excruciatingly painful even for a short and simple function call. It also introduces other problems, one of such as is pyramid of doom that makes it difficult to read and keep track of where the code is running.
func function(url: URL, completionBlock: (_ result: String) -> Void) {
callApi(url) { data in
decodeData(data) { decData in
loadImage(decData) { image in
completionBlock(image)
}
}
})
}
Another example is a short block that wil call an API and assign the result to a property:
func fetch(url: URL, completionBlock: (_ result: Dictionary<String, AnyObject>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
do {
// decode data
let json = try JSONSerialization.jsonObject(with: data!) as! Dictionary<String, AnyObject>
completionBlock(json)
} catch {
// handle error
}
}
}
The logic in this is very simple, but it is really long and harder to read. With code like this, it can also leave room for errors. This is because the framework Swift uses, Grand Central Dispatch or known as GCD, was designed for Objective-C and not for Swift. This made the code not very Swift-y.
Swift Concurrency
A few years after the release of Swift, Chris Lattner, one of the designers of Swift, released the Swift Concurrency Manifesto. Many developers, including me, are excited about the future of concurrency in Swift. It took a few revisions and years of preparation but back in late 2022, a proposal in Swift Evolution^2 has been made. Proposal SE-0296 was made by Swift’s co-designers John McCall and Doug Gregor and accepted with modifications by Ben Cohen as review manager for the proposal.
SE-0296 introduced a more Swift-y take for concurrency. It introduced asynchronous functions into Swift and allowed for writing complex functions in a much simpler way. It allowed asynchronous codes to be written in a straight-line as if it were a synchronous code.
func function(url: URL) async throws -> Image {
let data = try await callApi(url)
let decData = try await decodeData(data)
let image = try await loadImage(decData)
return image
}
The function code is now a lot cleaner than the previous code by marking them async
and returning a value rather than using and relying on completion handlers. It also removed a lot of syntax in the declaration of the function. It is much easier to read without the unnecessary pyramid of doom and definitely easier to handle errors lowering the probability of side effects.
Synchronicity in concurrency
Synchronous functions, when called, will immediately wait for the whole code block inside the function to complete. This is the default environment where concurrency is not supported. Once the function is complete, it will return control to the caller and picks up where it left off. The same thing happens when an asynchronous function is called: wait for the call to complete, control returns to the caller, and pick up where it left off. The only difference is that synchronous functions uses the same thread as everyone else while asynchronous functions uses their own separate threads.
Concurrency in Synchronicity
Because of their own limitations, synchronous functions can’t call asynchronous functions directly. This will give an error:
'async' call in a function that does not support concurrency
This error occurs when we try to call an asynchronous method inside a synchronous function that does not support concurrency. This works opposite when calling synchronous functions inside asynchronous functions, it will work normally and the function process will continue on the same thread the asynchronous function uses for the call.
We can use the Task
structure to call an asynchronous function.
func fetchSync() {
Task {
do {
try await fetchJSON()
} catch {
// handle errors here...
}
}
}
Note: Task functions can be called using the trailing closure.
A Task
is a unit of work that can be run asynchronously as part of the program. It is part of the concurrency framework introduced at WWDC 2021, along with async/await
. A task will allow us to create an environment that allows concurrency from a non-concurrent function. By default, a task executes on an automatically managed background thread.
Adapting concurrency
Code that takes too long to execute can negatively affect any app’s performance. When writing code to any language, all programs should run from start to finish without interruption. When calling a long running synchronous function, the user interface may need to stop working waiting for the function to finish. This won’t look good on the user side of things, and the user experience of the app will significantly plummet.
await
keyword
The await
expression is used for calling async methods. The await
keyword is placed in the beginning of the function’s call.
let data = try await callApi()
This keyword creates a suspension point where the execution of your code will pause until the asynchronous function or method returns. The use of `await` is restricted to type of `async` but it doesn’t have any other use, it merely marks that an asynchronous call is being made.
As any function that throws an error, the `throws` keyword can be added to implement functions that are asynchronous and can throw errors. This means `try await` will be repeated a lot. The developers have foreseen this and to reduce the boilerplate code, `await` could imply `try`.
Leave a Reply