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?

💥
The async function can be suspended in the middle of the execution when it’s waiting for something.

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.

💡
Suspension is never implicit or preemptive — each such place is marked with the 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?

💡
Do not use Swift Playgrounds to test new concurrency features as they are not fully supported yet

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 concurrency

That'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.

💡
To cancel a task, you can throw an error, return nil, or return partially completed work.Use 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
}
http://i.imgur.com/gyAFz.jpg

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?