Swift Async Await
08 Apr 2022In this article, we will explore Async/Await introduced in Swift 5.5.
This content is based on The Swift Programming Language (5.5) - Concurrency.
- Using asynchronous code makes it easy to perform multiple tasks concurrently in a program.
- Asynchronous code helps reduce code complexity.
1. Handling Asynchronous Tasks in the Past
Using the traditional completion handler-based asynchronous code, even simple code can become complex due to the need for nested closures.
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
}
2. Defining and Calling Asynchronous Functions
A new form of function called an “asynchronous function” allows pausing during execution. While traditional “synchronous functions” either perform a completion, throw an error, or do not return, asynchronous functions can pause during execution and resume when results arrive. You can declare an asynchronous function by adding the async
keyword at the end of the function declaration (before ->
, or before throws
if both are used).
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
}
When calling an asynchronous function, the function’s execution doesn’t proceed beyond the function call until it returns. Inside an asynchronous function, you can use the await
keyword to mark suspension points. The execution of an asynchronous function pauses only when there are additional asynchronous functions (with await
) within it. All suspension points must explicitly include the await
keyword.
let photoNames = await listPhotos(inGallery: "Summer Vacation")
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
let photo = await downloadPhoto(named: name)
show(photo)
The execution order of the above code is as follows:
- The code executes in the usual order until the
await
keyword is reached. It callslistPhotos(inGallery:)
, and the program waits until that function returns. - While the code is paused, the program can perform other concurrent tasks.
- After
listPhotos(inGallery:)
returns, the code resumes from the point it was paused. In this case, it assigns the return value tophotoNames
. - The lines containing
sortedNames
andname
do not haveawait
, so they behave like regular synchronous code. - Later,
downloadPhoto(named:)
is called withawait
, and the code pauses in a similar fashion to the first asynchronous call.
The await
keyword signifies that the code can potentially pause and wait for the asynchronous function to complete. Internally, Swift suspends the current thread, allowing other threads to execute, making it similar to yielding the thread. Because of this, the await
keyword can only be used in specific contexts:
- Inside an asynchronous function, method, or property body.
- Inside the
main()
function of a type annotated with@main
. - Inside detached child task code.
Note: You can use Task.sleep(_:) for easier testing of asynchronous functions.
3. Asynchronous Sequences
Similar to the traditional Sequence protocol for for-in
loops, Swift introduces the AsyncSequence protocol. This allows you to use a for-await-in
loop with asynchronous sequences.
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
}
In a for-await-in
loop, await
is executed at each iteration, and each iteration’s execution waits for the previous one to complete due to the suspension point.
4. Calling Asynchronous Functions in Parallel
Multiple independent asynchronous tasks can run concurrently by adding the async
keyword before a constant that will hold the task’s return value (async let
form). Constants defined in this manner can be used with await
.
async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])
let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)
In the above code, execution proceeds normally until the line where photos
is defined. At this point, the await
keyword is encountered, and the code pauses. Meanwhile, the downloadPhoto(named:)
calls are performed in parallel.
5. Task and Task Groups
A task is a unit of work that can be run asynchronously as part of your program.
All asynchronous code always runs as part of a Task. For example, the
async-let
constructs create child tasks for user convenience. Swift allows users to create their own Task Groups, add child tasks to them, and use them to manage concurrency effectively.
All tasks are part of a Task Group, and all tasks within a Task Group share the same parent task. Tasks can also have child tasks. This hierarchical relationship between tasks and Task Groups is referred to as Structured Concurrency.
To create a Task Group, you can use the withTaskGroup(of:returning:body:) API.
await withTaskGroup(of: Data.self) { taskGroup in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
taskGroup.addTask { await downloadPhoto(named: name) }
}
}
Unstructured Concurrency
Swift also supports creating tasks that are not part of a Task Group. In this case, you can use Task.init(priority:operation:) and Task.detached(priority:operation:) to create a new Task as a top-level Task.
- Tasks are used when you want to call asynchronous functions from synchronous code.
- When you want to create a Task with the caller’s priority and the current actor’s context, use Task.init(priority:operation:).
- When you want to create a Task that is separate from the current actor, use Task.detached(priority:operation:).
Task Cancellation
Each Task in Swift checks whether it has been canceled at appropriate points during its execution. A canceled Task can either throw an error, return an empty collection or nil, or return a partially completed task. You can check for cancellation using Task.checkCancellation() or Task.isCancelled.