This is yet another way to do concurrency, this time in Swift 5.5 with much cleaner code. First introduced in WWDC 2021, but it won’t be the last we see of it.
Implement async
functions
Simply declare with the async
keyword. It is also common to have throws
right after it (so as to throw error instead of returning in the old Result tuple).
func asyncHello() async throws -> String {
await Task.sleep(1_000_000_000) // 1 second
return "Hello World"
}
It is also common to replace your existing functions with completion handlers by using withCheckedContinuation
/withUnsafeContinuation
.
func newAsyncFunc() async throws -> String {
await withCheckedContinuation { continuation in
existingOldFunc { (items, error) in
if let error = error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: items)
}
}
}
}
Use await
to call async functions
do {
let result = try await asyncHello()
} catch {
log(error)
}
The keyword await
indicates there is possible suspension. To be able to suspend execution, only certain part of the code can call it, namely:
async
function@main
,@MainActor
- Detached
Task
Sequentially or in parallel
The beauty is that you can have multiple await
calls, and they will run line after line, sequentially.
let result1 = try await asyncHello() //
let result2 = try await asyncHello() // Runs after result1 yield
To run in parallel, use async let
.
async let result1 = try downloadPhoto(1)
async let result2 = try downloadPhoto(2)
let results = await [result1, result2]
AsyncSequence
This is for sequence of values arriving over time. Sounds familiar? Yes, the use case is similar to Combine.
You iterate an AsyncSequence
as per normal, but with an await
.
for await str in asyncLinesOfText {
...
}
Structured Currency
It is structured because each Task
can have child tasks – they have explicit relationships.
You can also detach task, and becomes unstructured.
A TaskGroup
can create dynamic number of task. An example from WWDC:
actor
actor
is similar to class
, except it allows only one task to access the mutable state at a time, which makes it safe for code in multiple tasks.
If you access an actor property from the outside, you need to use await
.
A useful global actor wrapper is @MainActor
. Declare a func with it, and the func will run on main thread.
Running on main thread
There are a few ways to ensure your code is on the main thread.
- Annotate
@MainActor
on a class or func Task { @MainActor in ... }
await MainActor.run { ... }
Running on non-main thread
One way is to use Task.detached { ... }
. However, note that detached task is unstructured concurrency, which means you need to handle cancellation yourself. It is a “last resort”.
If you need to perform some heavy work on non-main thread, use good old DispatchQueue
. You can have an async func that runs on a custom serial queue like this:
private let queue = DispatchQueue(label: "my.serial.queue", qos: .userInitiated)
func doLongRunningOnQueue() async -> String {
await withCheckedContinuation { continuation in
queue.async {
...
continuation.resume(returning: "foo")
}
}
}
This is the way until we can specify the executor for a Task.
Switching task
You can call await Task.yield()
to allow the current task to switch to another task. By yielding, you can ensure your queue won’t be blocking.