Utilities for serializing asynchronous operations
Requirements: iOS 16.0+ / macOS 10.15+ • Swift 6.1+ / Xcode 16.4+
All applications I worked on had to serialize some asynchronous jobs at some point. This is frequent in apps that deal with a shared mutable resource, such as a remote server: I'd rather make sure that all network requests that deal with a given resource have completed before I start a new one.
The Swift standard library does not provide any ready-made solution for this task. One has to build their own serialization on top of, say, AsyncStream
:
Serialization with `AsyncStream`
// A program that prints 1, 2, 3, in this order.
// Setup
typealias Operation = @Sendable () async -> Void
let (stream, continuation) = AsyncStream.makeStream(of: Operation.self)
Task {
for await operation in stream {
await operation()
}
}
// Serialize operations
continuation.yield { print("1") }
continuation.yield { print("2") }
continuation.yield { print("3") }
// Cleanup
continuation.finish()
Such hand-made code gets more and more complicated as application needs grow:
- How to wait for an operation to complete? How to return a result from an operation?
- How to deal with both throwing and non-throwing operations?
- How to deal with cancellation? How to cancel one particular operation without cancelling other ones?
- How to deal with the cancellation of non-throwing operations, since they can not throw
CancellationError
?
This package comes with three "queues" that serialize asynchronous operations. Enqueued operations run one after the other, in order, without overlapping.
They differ in their way to handle task cancellation:
-
AsyncQueue
can run both throwing and non-throwing operations. A cancelled operation can only handle cancellation when it runs, i.e. after the completion of previously enqueued operations. -
DiscardingAsyncQueue
eagerly discards cancelled operations, without waiting for the completion of previously enqueued operations. All operations may throwCancellationError
. -
CoalescingAsyncQueue
eagerly discards cancelled operations, likeDiscardingAsyncQueue
. It can also coalesce operations by cancelling "discardable" operations that are replaced by another operation.
💡 If your app does not intend to cancel operations, use AsyncQueue
. You won't have to deal with errors for non-throwing operations.
💡 If your app has to deal with cancellation, DiscardingAsyncQueue
helps cancelled operations complete as early as possible. In exchange, you'll have to deal with errors even for non-throwing operations.
💡 If your app runs operations that can be discarded without consequences, consider CoalescingAsyncQueue
.
All queues have a similar API:
-
addTask()
returns a new top-level task, which you can await if you want:let task = queue.addTask { try await doSomething() } let result = try await task.value
-
perform()
returns the result of an async operation.let value = try await queue.perform { try await someValue() }
For example:
// Prints 1, 2, 3, in this order.
let queue = AsyncQueue()
queue.addTask { print("1") }
queue.addTask { print("2") }
await queue.perform { print("3") }