Written by: Agustin Rieppi, Felipe Miraballes, and Mauro Curbelo.
In this post, we’ll analyze and compare two new technologies for asynchronous programming. The first one is called async/await, available for the Swift programming language, and the second one is Coroutines, available for Kotlin. Our comparisons will be based on the iOS and Android development points of view.
Both technologies strive to resolve the same problem using similar approaches, based on the concept of Coroutines. We’ll try to understand similarities and differences, advantages and disadvantages.
Before we head further into comparison territory, let’s take a step back and review each of these new tools separately to understand them better.
Coroutines offers lightweight support for function suspension and simplicity when writing asynchronous code. It’s based on structured concurrency to execute operations within the same scope. It’s important to note that the tool is built upon Java’s existing Thread handling framework.
The main goals of Coroutines are to improve Long-Running Tasks and offer Main-safety, allowing suspend functions to be called from the main Thread with minimal risk, all this while improving performance and simplifying syntax.
Today, Coroutines is integrated and fully supported by most Jetpack Android libraries, guaranteeing compatibility with a wide variety of tools.
Async/Await was first introduced in 2021’s WWDC by Apple as the new concurrency library for its Swift programming language. It incorporates support for both synchronous and asynchronous code using a structured syntax. Unlike the way it was previously done (Grand Central Dispatch, GCD for short), async/await is entirely written in Swift, which allows the compiler to assist the programmer at writing time.
The main goal of async/await is to solve some of the problems with GCD, mainly its complex and error-prone syntax.
As we have already mentioned, one of the main things these tools try to fix is syntax complexity, so it’s essential to compare them. First, we’ll start by comparing syntax between what we had before and what these technologies introduce. Then, we’ll make a comparison between them for similarities and differences.
As you can see in this comparison, not only do we have fewer lines of code, but it’s also significantly less complex.
Next, we’ll see a few simple code snippets illustrating how a few simple operations would look. In both examples, the functions do the same; what changes is the context they can be called from. In our first example, a suspend/async function can only be called from another suspend/async function, whereas the second example can be called from an otherwise synchronous function context.
Right off the bat, we can see that method signatures are simple and easily convey their purpose. The main difference between them is that while Async/await has a specified return value, Coroutines leans more towards updating existing data.
Task lifecycle and the way to terminate them have plenty of similarities. To visualize, let’s consider this scenario. A user initiates a background task, but before it finishes, they close the screen. We don’t need this task to finish because the result is no longer relevant; thus, we need to cancel the task to avoid wasting resources.
Both async/await and Coroutines implement cooperative cancellation. When a task is asked to cancel itself, it will wait for all subtasks to finish before doing so. Subtasks can end normally, or at reasonable times verify if the cancellation of their parent was requested.
Both technologies provide idiomatic, straightforward ways to do this:
In both cases, it is up to the caller to handle the exceptions raised by a child task being canceled.
In Coroutines’ case, some preexisting scopes are linked with the lifecycle of a specific component. For example, viewModelScope and all its subtasks will be canceled if the view model is garbage collected.
A relatively common use case: Fetching several values through time in a non-blocking way.
Async/await has a solution for this scenario out of the box, whereas Coroutines doesn’t.
However, Coroutines does offer a separate library for this, called Coroutines Flow (A theme around Coroutines. There are several add-on libraries, presumably to not bloat the main package with unnecessary APIs).
In our brief example, we’ll read the text file line by line until one contains the word “Exit.”
When you have several different asynchronous operations running simultaneously, you might want to prioritize one over the rest.
Async/await has the concept of task priority with different options. Here they are listed in a descending priority.
- High (userInitiated)
- Low (utility)
And this is a small code snippet of how it would look like implemented:
In Coroutines’ case, there’s no idea of different priorities for each coroutine, but it has the concept of Dispatchers.
Dispatchers are tasked with delivering each coroutine to a different set of threads optimized for different types of operations.
The main dispatchers are
- Dispatchers.Main – Runs on the Main Thread, mainly for UI operations and light work.
- Dispatchers.IO – Optimized for IO operations, doesn’t utilize the Main Thread.
- Dispatchers.Default – Optimized for CPU-intensive work.
Individual tasks can also yield their thread time through the usage of the yield() method.
While both tools use slightly different concepts, they offer a way to differentiate tasks depending on the work being done.
They have very similar syntax, but Async/await naming could be said to be clearer and more intuitive, although this is very subjective.
Throughout the writing of this blog, we realized that these libraries share many similarities, and we believe that for people who have a certain amount of knowledge of one, it can translate to the other one to a certain degree.
Our developers who had prior knowledge of Coroutines had an easier time adjusting to Async/await than those who didn’t. One of the most significant downsides of Async/Await at the time of writing is the fact that it is only available for iOS 15+, which comprises a minority of users. However, it is a massive upgrade from the previous method of writing asynchronous code. It’s a way more flexible, safe, and readable alternative to GCD.
Update: Xcode 13.2 beta incorporates backward compatibility for the new Swift Concurrency, allowing to use it from iOS 13.
As for Coroutines, its main advantage lies in the fact that each coroutine adds little overhead costs compared to creating an entirely new thread. In applications where we need a lot of concurrent operations, this is an excellent alternative.
If you like Android or iOS Programming (or both!), you should definitely give them a try, as it looks like they’re the way forward concurrency-wise in their respective platforms.