Contents

dfed/swift-async-queue

A library of queues that enable sending ordered tasks from nonisolated to asynchronous contexts.

Task Ordering and Swift Concurrency

Tasks sent from a nonisolated context to an asynchronous context in Swift Concurrency are inherently unordered. Consider the following test:

```swift
@Test
func actorTaskOrdering() async {
    actor Counter {
        func incrementAndAssertCountEquals(_ expectedCount: Int) {
            count += 1
            let incrementedCount = count
            #expect(incrementedCount == expectedCount) // often fails
        }

        private var count = 0
    }

    let counter = Counter()
    var tasks = [Task<Void, Never>]()
    for iteration in 1...100 {
        tasks.append(Task {
            await counter.incrementAndAssertCountEquals(iteration)
        })
    }
    // Wait for all enqueued tasks to finish.
    for task in tasks {
        _ = await task.value
    }
}
```

Because the `Task` is spawned from a nonisolated execution context, the ordering of the scheduled asynchronous work is not guaranteed.

While [actors](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID645) are great at serializing tasks, there is no simple way in the standard Swift library to send ordered tasks to them from a nonisolated synchronous context, or from multiple execution contexts.

### Executing asynchronous tasks in FIFO order

Use a `FIFOQueue` to execute asynchronous tasks enqueued from a nonisolated context in FIFO order. Tasks sent to one of these queues are guaranteed to begin _and end_ executing in the order in which they are enqueued. A `FIFOQueue` executes tasks in a similar manner to a `DispatchQueue`: enqueued tasks execute atomically, and the program will deadlock if a task executing on a `FIFOQueue` awaits results from the queue on which it is executing.

A `FIFOQueue` can easily execute asynchronous tasks from a nonisolated context in FIFO order:
```swift
@Test
func fIFOQueueOrdering() async {
    actor Counter {
        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) -> Task<Void, Never> {
            Task(on: queue) {
                await self.increment()
                let incrementedCount = await self.count
                #expect(incrementedCount == expectedCount) // always succeeds
            }
        }

        func increment() {
            count += 1
        }

        private var count = 0
        private let queue = FIFOQueue()
    }

    let counter = Counter()
    var tasks = [Task<Void, Never>]()
    for iteration in 1...100 {
        tasks.append(counter.incrementAndAssertCountEquals(iteration))
    }
    // Wait for all enqueued tasks to finish.
    for task in tasks {
        _ = await task.value
    }
}
```

FIFO execution has a key downside: the queue must wait for all previously enqueued work – including suspended work – to complete before new work can begin. If you desire new work to start when a prior task suspends, utilize an `ActorQueue`.

### Sending ordered asynchronous tasks to Actors from a nonisolated context

Use an `ActorQueue` to send ordered asynchronous tasks to an `actor`'s isolated context from nonisolated or synchronous contexts. Tasks sent to an actor queue are guaranteed to begin executing in the order in which they are enqueued. However, unlike a `FIFOQueue`, execution order is guaranteed only until the first [suspension point](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID639) within the enqueued task. An `ActorQueue` executes tasks within its adopted actor's isolated context, resulting in `ActorQueue` task execution having the same properties as `actor` code execution: code between suspension points is executed atomically, and tasks sent to a single `ActorQueue` can await results from the queue without deadlocking.

An instance of an `ActorQueue` is designed to be utilized by a single `actor` instance: tasks sent to an `ActorQueue` utilize the isolated context of the queue‘s adopted `actor` to serialize tasks. As such, there are a couple requirements that must be met when dealing with an `ActorQueue`:
1. The lifecycle of any `ActorQueue` should not exceed the lifecycle of its `actor`. It is strongly recommended that an `ActorQueue` be a `private let` constant on the adopted `actor`. Enqueuing a task to an `ActorQueue` instance after its adopted `actor` has been deallocated will result in a crash.
2. An `actor` utilizing an `ActorQueue` should set the adopted execution context of the queue to `self` within the `actor`’s `init`. Failing to set an adopted execution context prior to enqueuing work on an `ActorQueue` will result in a crash.

An `ActorQueue` can easily enqueue tasks that execute on an actor’s isolated context from a nonisolated context in order:
```swift
@Test
func actorQueueOrdering() async {
    actor Counter {
        init() {
            // Adopting the execution context in `init` satisfies requirement #2 above.
            queue.adoptExecutionContext(of: self)
        }

        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) -> Task<Void, Never> {
            Task(on: queue) { myself in
                myself.count += 1
                #expect(expectedCount == myself.count) // always succeeds
            }
        }

        private var count = 0
        // Making the queue a private let constant satisfies requirement #1 above.
        private let queue = ActorQueue<Counter>()
    }

    let counter = Counter()
    var tasks = [Task<Void, Never>]()
    for iteration in 1...100 {
        tasks.append(counter.incrementAndAssertCountEquals(iteration))
    }
    // Wait for all enqueued tasks to finish.
    for task in tasks {
        _ = await task.value
    }
}
```

### Sending ordered asynchronous tasks to the `@MainActor` from a nonisolated context

Use `MainActor.queue` to send ordered asynchronous tasks to the `@MainActor`’s isolated context from nonisolated or synchronous contexts. Tasks sent to this queue type are guaranteed to begin executing in the order in which they are enqueued. The `MainActor.queue` is an `ActorQueue` that runs within the `@MainActor` global context: execution order is guaranteed only until the first [suspension point](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#ID639) within the enqueued task. Similarly, code between suspension points is executed atomically, and tasks sent to the `MainActor.queue` can await results from the queue without deadlocking.

A `MainActor.queue` can easily execute asynchronous tasks from a nonisolated context in FIFO order:
```swift
@MainActor
@Test
func mainActorQueueOrdering() async {
    @MainActor
    final class Counter {
        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) -> Task<Void, Never> {
            Task(on: MainActor.queue) {
                self.increment()
                let incrementedCount = self.count
                #expect(incrementedCount == expectedCount) // always succeeds
            }
        }

        func increment() {
            count += 1
        }

        private var count = 0
    }

    let counter = Counter()
    var tasks = [Task<Void, Never>]()
    for iteration in 1...100 {
        tasks.append(counter.incrementAndAssertCountEquals(iteration))
    }
    // Wait for all enqueued tasks to finish.
    for task in tasks {
        _ = await task.value
    }
}
```

### Cancelling all executing and pending tasks

Use a `CancellableQueue` to wrap a `FIFOQueue` or `ActorQueue` when you need the ability to cancel all currently executing and pending tasks at once. This is useful for scenarios like cancelling in-flight network requests when a view disappears, or abandoning a batch of operations when a user initiates a new action.

A `CancellableQueue` wraps an underlying queue and tracks all tasks enqueued on it. Calling `cancelTasks()` will cancel both the currently executing task and any tasks waiting in the queue. Tasks that have already completed are unaffected, and tasks enqueued after `cancelTasks()` is called will execute normally.

```swift
@Test
func cancellableQueueExample() async {
    actor ImageLoader {
        init() {
            let actorQueue = ActorQueue<ImageLoader>()
            cancellableQueue = CancellableQueue(underlyingQueue: actorQueue)
            actorQueue.adoptExecutionContext(of: self)
        }

        nonisolated
        func loadImage(from url: URL) -> Task<UIImage?, Never> {
            Task(on: cancellableQueue) { myself in
                guard let image = try await myself.fetchImage(from: url) else { return nil }
                try Task.checkCancellation()
                return myself.processImage(image)
            }
        }

        nonisolated
        func cancelAllLoads() {
            // Cancels the currently loading image and any queued load requests.
            cancellableQueue.cancelTasks()
        }

        private func fetchImage(from url: URL) async throws -> UIImage? {
            // Fetch image implementation…
        }

        private func processImage(_ image: UIImage) async -> UIImage {
            // Expensive image processing implementation…
        }

        private let cancellableQueue: CancellableQueue<ActorQueue<ImageLoader>>
    }

    let loader = ImageLoader()

    // Enqueue several image load tasks.
    let task1 = loader.loadImage(from: URL(string: "https://example.com/1.png")!)
    let task2 = loader.loadImage(from: URL(string: "https://example.com/2.png")!)
    let task3 = loader.loadImage(from: URL(string: "https://example.com/3.png")!)

    // Cancel all pending and executing loads.
    loader.cancelAllLoads()

    // All tasks are now cancelled.
    #expect(task1.isCancelled)
    #expect(task2.isCancelled)
    #expect(task3.isCancelled)
}
```

A `CancellableQueue` can also wrap a `FIFOQueue` for FIFO-ordered cancellable tasks:
```swift
let cancellableQueue = CancellableQueue(underlyingQueue: FIFOQueue())

Task(on: cancellableQueue) {
    // This work can be cancelled via cancellableQueue.cancelTasks()
    await performWork()
}
```

Installation

Swift Package Manager

To install swift-async-queue in your project with Swift Package Manager, the following lines can be added to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/dfed/swift-async-queue", from: "1.0.0"),
]

CocoaPods

To install swift-async-queue in your project with CocoaPods, add the following to your Podfile:

pod 'AsyncQueue', '~> 1.0.0'

Contributing

I’m glad you’re interested in swift-async-queue, and I’d love to see where you take it. Please read the contributing guidelines prior to submitting a Pull Request.

Thanks, and happy queueing!

Developing

Double-click on Package.swift in the root of the repository to open the project in Xcode.

Package Metadata

Repository: dfed/swift-async-queue

Default branch: main

README: README.md