Video: Meet async/await in Swift
With Swift concurrency, functions, initializers, read-only properties, and for-loops can all be marked async
. Property getters can also throw.
func fetchThumbnails() await throws -> [UIImage] {
}
extension UIImage {
var thumbnail: UIImage {
get async throws {
let size = CGSize(width: 50, height: 50)
return await byPreparingThumbnail(ofSize: size)
}
}
}
for await id in imageIDs {
let thumbnail = await fetchThumbnail(withID: id)
}
async
enables a function to be suspended while the task is performed.
await
marks where an async function may be suspended. The thread that suspends for an await
call is not blocked, and can perform other work.
Tests support async/await.
func testSomething() async throws {
let result = try await object.someAsyncThing()
XCTAssertEqual(result, expectedResult)
}
Continuations provide a bridge to older completion handler based code into new async based methods.
func persistentPosts() async throws -> [Post] {
typealias PostsContinuation = CheckedContinuation<[Post], Error>
return try await withCheckedContinuation { (continuation: PostsContinuation) in
// perform call-back based request
self.getPosts { posts, error in
if let error = error {
continuation.resume(throwing: error)
}
else {
continuation.resume(returning: posts)
}
}
}
}
Resumes must only be called once for each path. Discarding the continuation without resuming is not allowed.
Continuations can be stored as properties and called outside of the defining scope to support behaviors such as working with delegate callbacks.
The following async let
and group tasks demonstrate structured concurrency where tasks can be executed within other tasks, and also provide the ability to cancel tasks while propogating that to other tasks in the structure.
Allows a concurrent creation and setting of a variable. The code following the declaration of the async let
variable will be allowed to execute until the value of the async let
variable needs to be read.
async let remoteData = MyDataLoader.load(from: url)
// ... values can execute until it is used:
let count = remoteData.count
Video: Explore structured concurrency in Swift
To allow multiple concurrent tasks to execute together as a group, nest them within a group task. Each sub-task is invoked by calling group.async { ... }
within the group.
var thumbnails = [String: UIImage]()
try await withThrowingTaskGroup(of: Void.self) { group in
for id in ids {
group.async {
thumbnails[id] = try await fetchThumbnail(withID: id)
}
}
}
The above code has an issue though: concurrent access to the thumbnails
property. That structure is not thread-safe, so we need to fix the issue using by returning values to the task group, and then update the final storage with a new for try await
loop.
var thumbnails = [String: UIImage]()
try await withThrowingTaskGroup(of: (String, UIImage).self) { group in
for id in ids {
group.async {
return (id, try await fetchThumbnail(withID: id))
}
}
// outside of the async group sub-tasks, coalesce the results into the final thumbnails dictionary:
for try await (id, thumbnail) in group {
thumbnails[id] = thumbnail
}
}
return thumbnails
Video: Meet async sequence
Check if a task has been cancelled during the scope of an async
method:
if Task.isCancelled { break }
or
try Task.checkCancellation()
This is useful when grouping tasks, or checking to see if a task within multiple calls to throwing async
methods has failed, and we no longer want to continue additional work.
Groups can also be cancelled:
group.cancelAll()
func collectionView(_ view: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
Task {
let thumbnails = await fetchThumbnails(forItem: item)
displayThumbnails(thumbnails, in: cell)
}
}
This task will execute the blocking fetchThumbnails
method on the thread it was called on. In this case, the main thread since it was from a collection view delegate method. However, the calling scope is not blocked and is allowed to continue execution. The task will be executed later when it is efficient to do so.
The lifetime of the Task
is not limited to the calling scope. It will live on until the await is fulfilled. Can be manually cancelled or awaited. Below is an example of cancelling the task to fetch thumbnails for a collection view cell after it is scrolled out of view:
var thumbnailTasks = [IndexPath: Task<Void, Never>]()
func collectionView(_ view: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
thumbnailTasks[indexPath] = Task {
defer { thumbnailTasks[indexPath] = nil } // clear task from storage once completed
let thumbnails = await fetchThumbnails(forItem: item)
displayThumbnails(thumbnails, in: cell)
}
}
func collectionView(_ view: UICollectionView, willEndDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
thumbnailTasks[indexPath].cancel()
thumbnailTasks[indexPath] = nil
}
Unstructured tasks that have unscoped lifetime, manually cancelled and awaited, and do not inherit their originating context, e.g. the thread which it was invoked. These run independently, and can be configured with custom priority and other traits.
Task.detatched(priority: .background) {
writeThumbnailsToCache(thumbnails)
}
Detached tasks can also make use of structured tasks within them, gaining the advantages of structured concurrency.
Task.detatched(priority: .background) {
withTaskGroup(of: Void.self) { group
group.async { writeThumbnailsToCache(thumbnails) }
group.async { log(thumbnails) }
group.async { ... }
}
}
If the background detatched group is cancelled, all sub-tasks will be canceled as well. These sub tasks will also inherit the priority of the parent task.
Video: Protect mutable state with Swift actors
Actors provide synchronization for shared mutable state. Actors isolate their state from the rest of the program. The actor will ensure mutally-exclusive access to its state.
actor Counter {
var value = 0
func increment() -> Int {
value = value + 1
return value
}
}
Actors are similar to structs, classes, and enums, but are their own fundamental reference type. They can conform to protocols and be extended with extensions. Their defining characteristic is that they provide synchronization and isolation of its state.
Calls within an actor are perform synchronously and run uninterrupted.
extension Counter {
func resetSlowly(toValue value: Int) {
value = 0 // access is synchronous
for _ in 0..<value {
increment() // call is synchronous
}
}
}
Calls from outside are asynchronous, and can be awaited.
let counter = Counter()
await counter.increment()
Within the actor, we may use await calls that may suspend the method. Be sure to note that the state of the actor can change by the time the awaited call completes. Do not assume that that the state is the same, and make sure to check for mutations that may have happened during suspension. These assumptions should be checked after resuming from an await
.
nonisolated
keyword. Methods decorated with these keyword are defined as being "outside" of the actors controlled state. These methods cannot access mutable state on the actor, since it can be accessed outside of the controlled state. The following example shows how to conform to Hashable
by using a nonisolated
implementation of the hash(into:)
method that uses an immutable id property. Using a mutable property in the method would lead to a compiler error.
actor User {
let id: String
}
extension User: Hashable {
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
In the case where we have non-isolated closures that call back to mutate the actor, we need to ensure that we re-enter the actor in a safe way using await
.
extension LibraryAccount {
func read() -> Int { … }
func readLater() {
Task.detatched {
await self.read()
}
}
}
Sendable
types are objects that are safe to share concurrently. It is a protocol and can be conformed to.
Classes for example are typically not safe because other objects can mutate their state at the same time and cause a race condition. Classes can only be sendable when their state is immutable, or interally performs synchronization. Value types are sendable because they can be modified independently since they are copied with each instance. Actor types are always sendable.
Functions can be sendable, but not always, and may be defined as such with the @Sendable
keyword. This keyword is used to indicate where concurrent execution can occur, and prevents data races when accessing captured variables.
This is important for closures when capturing variables. Sendable functions cannot capture mutable variables, and can only capture other Sendable
variables. Sendable functions cannot be both synchronous and actor-isolated.
Detatched tasks required sendable closures, and is defined like so:
static func detatched(operation: @Sendable () async -> Success) -> Task<Success, Never>
An actor that executes solely on the main thread. We can decorate functions, classes and actors with @MainActor
to indicate its work is performed on the main thread. It implies that all methods and properties are run on the main thread.
@MainActor func updateUI(title: String) {
…
}
// if called from outside of the main thread, it must be awaited
await updateUI(title: "Sweet")
@MainActor class MyViewController: UIViewController {
func onPress(…) { … } // implicitly called on main thread
// methods can opt out of the main thread using `nonisolated`
nonisolated func fetchLatestAndDisplay() async { … }
}
Sequences can be iterated asychronously. This is done with an iterator that uses a async method to return results to a loop. The loop can use the await keyword to handle each element as they are provided.
for await quake in quakes { … }
do {
for try awake quake in quakes { … }
} catch {
…
}
These can also be placed within tasks to prevent the calling thread from suspending, or if they run indefinitely:
Task {
for await quake in quakes { … }
}
They can also be cancelled:
let iteration = Task {
for await quake in quakes { … }
}
// …later
iteraction.cancel()
FileHandle
objects can now provide bytes in an async fashion:
public var bytes: AsyncBytes
for try await line in FileHandle.standardInput.bytes.lines {
}
You can also asynchronously access bytes or lines from a URL
:
public var resourceBytes: AsyncBytes
public var lines: AsyncLineSequence<AsyncBytes>
let url = URL(fileURLWithPath: "/path/to/file.txt")
for try await line in url.lines { … }
Video: Use async/await with URLSession
You can wait for notifications to be delivered:
let notification = await NotificationCenter.default.notifications(named: .NSPersistentStoreChange).first {
$0.userInfo[NSStoreUUIDKey] == storeUUID
}
AsyncStream
can be used to adapt existing callback patterns to provide a stream of values compatible with swift concurrency.
let quakes = AsyncStream(Quake.self) { continuation in
let monitor = QuakeMonitor()
monitor.quakeHandler = { quake in
continuation.yield(quake)
}
continuation.onTermination = { _ in
monitor.stopMonitoring()
}
monitor.startMonitoring()
}
The AsyncStream
can then be iterated over using for await
as values are provided.
Use AsyncThrowingStream
to produce a stream of values that can also throw.