Quick Guide to Async Await in Swift
How to create asynchronous functions, run code in parallel, who is MainActor, what is the closures pyramid and how to get rid of it? Let's start.
Straight to the point
Swift 5.5 introduced built-in support for writing asynchronous and parallel code in a structured way. Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time.
Keyword async
is used to mark function as asynchronous. That's it.
func downloadNames(fromServer name: String) async -> [String] {
... // some other tasks
return data
}
But what does it really mean?
Here's how async
functions can be called
let namesMain = await downloadNames(fromServer: "main")
let secondary = await downloadNames(fromServer: "secondary")
When you type await
, the current execution is suspended, until an asynchronous call is finished.
await
keyword.Where to call async functions
As I said before, await
suspends current execution. But there must be a structure underneath that can be suspended. You can't suspend a raw thread or the main thread, for example.
You opened Playgrounds, right?
If you try to call an async function in an inappropriate place, you will see this error
async
call in a function that does not support concurrencyThat's because an asynchronous function can be called only in:
- 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
That's a lot of words.
For most developers, only the first and the last points make sense. Most of the places in your code do not support await
. How to deal with that?
Tasks and TaskGroup
Task
To call an asynchronous function in a place that does not support concurrency, you need to create a concurrent task. You can use Task
and TaskGroup
to achieve that.
Task {
let names = await downloadNames(fromServer: "main")
... // futher work
... // take over the world (asynchronously)
}
When you create an instance of Task
, you provide a closure that contains the work for that task to perform. Tasks can start running immediately after creation and may not. You can create a task in another Task
or other concurrent environments.
let handle = Task { // Creates asynchronous task
let names = await downloadNames(fromServer: "main")
Task { // Creates asynchronous task
await save(names: names)
}
for name in names {
print(name)
}
}
After creating a task, you use the instance to interact with it — for example, to wait for it to complete or to cancel it. Tasks run independently from their handles.
Task.isCancelled
to check if the current task was cancelled.TaskGroup to group the tasks
TaskGroup
lets you launch several tasks and wait for the completion of all of them. The order in which these tasks are completed is not defined.
How to create it?
TaskGroup
is created through withTaskGroup(of:)
. You provide closure in which you spawn new tasks and perform operations on returned data.
let calculations = await withTaskGroup(of: Int.self) { group -> Int in
group.addTask { 1 * 2 } // () -> Int
group.addTask { 2 * 3 }
group.addTask { 3 * 4 }
group.addTask { 4 * 5 }
group.addTask { 5 * 6 }
var collected = [Int]()
for await value in group {
collected.append(value)
}
return collected
}
The group
object inside closure conforms to AsyncSequence
. It's just like a general sequence, but elements are generated asynchronously. To iterate over it you can use .next()
method or for await ... in sequence
.
It can be used to parallelize for
loops, for example.
let calculations = await withTaskGroup(of: Int.self) {[works] group -> [Int] in
for work in works {
group.addTask { work() }
}
var collected = [Int]()
for await value in group {
collected.append(value)
}
return collected
}
That's great, but how to perform unrelated tasks concurrently without TaskGroup?
Async let, async get, concurrent execution
These features seem like a real power to me.
Imagine you need to load an article, and data stored on different services or URLs:
- Article thumbnail
- Article text
- Related articles
- Comments
And the most obvious way to load all data is to write such code
let thumbnail = await loadThumbnail(forPost: post)
let text = await loadArticleText(forPost: post)
let related = await loadRelatedArticles(forPost: post)
let comments = await loadComments(forPost: post)
And this is mighty concurrent code that will load needed information the fastest way. Right? Not really.
Code is still executed serially and assets are not loaded in parallel. Each step waits until data is loaded. You can spawn a task for every step, sure. But is it really a nice solution?