AsyncQueue 版本 0.5.2

AsyncQueue 版本 0.5.2

Dan Federman 维护。



AsyncQueue 版本 0.5.2

  • 作者
  • Dan Federman

swift-async-queue

CI Status codecov License

一个队列库,允许将有序任务从同步环境发送到异步环境。

任务排序与 Swift 并发

在 Swift Concurrency 中,从同步环境向异步环境发送的任务通常是未排序的。考虑以下测试

@MainActor
func testMainActorTaskOrdering() async {
    actor Counter {
        func incrementAndAssertCountEquals(_ expectedCount: Int) {
            count += 1
            let incrementedCount = count
            XCTAssertEqual(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
    }
}

尽管产生的 Task 继承了序列 @MainActor 执行上下文,但计划中的异步工作排序不可保证。

虽然 actors 很擅长序列化任务,但在标准的 Swift 库中没有简单的方法可以将其从同步环境发送给它们。

以 FIFO 顺序执行异步任务

使用一个 FIFOQueue 以 FIFO 顺序执行来自非隔离上下文队列的异步任务。发送到这些队列的任务将保证按它们入队的顺序开始和结束执行。FIFOQueueDispatchQueue 的执行方式类似:入队任务原子执行,如果在 FIFOQueue 上执行的任务等待其在执行的任务队列的结果,程序将发生死锁。

FIFOQueue 可以轻松地按 FIFO 顺序从非隔离上下文执行异步任务

func testFIFOQueueOrdering() async {
    actor Counter {
        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) {
            queue.enqueue {
                await self.increment()
                let incrementedCount = await self.count
                XCTAssertEqual(incrementedCount, expectedCount) // always succeeds
            }
        }

        func flushQueue() async {
            await queue.enqueueAndWait { }
        }

        func increment() {
            count += 1
        }

        var count = 0

        private let queue = FIFOQueue()
    }

    let counter = Counter()
    for iteration in 1...100 {
        counter.incrementAndAssertCountEquals(iteration)
    }
    // Wait for all enqueued tasks to finish.
    await counter.flushQueue()
}

FIFO 执行的关键缺点是队列必须等待所有之前入队的工作(包括挂起的工作)完成后才能开始新工作。如果您希望前一个任务挂起时开始新工作,请使用 ActorQueue

从非隔离上下文向演员发送有序的异步任务

使用 ActorQueue 将有序的异步任务发送到 actor 的隔离上下文,源自非隔离或同步上下文。发送到 actor 队列的任务将保证按它们入队的顺序开始执行。然而,与 FIFOQueue 不同,执行顺序只在入队任务中的第一个挂起点(suspension point)之前得到保证。ActorQueue 在其采用的 actor 的隔离上下文中执行任务,因此 ActorQueue 任务执行的属性与 actor 代码执行相同:挂起点之间的代码原子执行,并且可以向单个 ActorQueue 发送任务而不会死锁。

ActorQueue 的一个实例是为单个 actor 实例设计的:发送到 ActorQueue 的任务使用队列采用的 actor 的隔离上下文来序列化任务。因此,在处理 ActorQueue 时必须满足一些要求

  1. 任何 ActorQueue 的生命周期不应超过其 actor 的生命周期。强烈建议将 ActorQueue 作为采用的 actor 上的 private let 常量。在其采用的 actor 被解除分配后,向 ActorQueue 实例入队任务会导致崩溃。
  2. 使用 ActorQueueactor 应在 actorinit 中将队列的采用的执行上下文设置为 self。在向 ActorQueue 入队工作之前没有设置采用的执行上下文会导致崩溃。

ActorQueue 可以轻松地将任务有序地入队到 actor 的隔离上下文中,从非隔离上下文执行

func testActorQueueOrdering() async {
    actor Counter {
        init() {
            // Adopting the execution context in `init` satisfies requirement #2 above.
            queue.adoptExecutionContext(of: self)
        }

        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) {
            queue.enqueue { myself in
                myself.count += 1
                XCTAssertEqual(expectedCount, myself.count) // always succeeds
            }
        }

        func flushQueue() async {
            await queue.enqueueAndWait { _ in }
        }

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

    let counter = Counter()
    for iteration in 1...100 {
        counter.incrementAndAssertCountEquals(iteration)
    }
    // Wait for all enqueued tasks to finish.
    await counter.flushQueue()
}

将有序异步任务从非隔离环境发送到 @MainActor

使用 MainActorQueue 将有序异步任务从非隔离或同步环境发送到 @MainActor 的隔离上下文。发送到这种队列类型的任务将保证按入队顺序开始执行。与 ActorQueue 一样,执行顺序保证仅限于入队任务中的第一个 挂起点。在它采用的角色的隔离上下文中执行 MainActorQueue 任务,因此 MainActorQueue 任务执行具有与 @MainActor 代码执行相同的属性:挂起点之间的代码是原子执行的,并发送到单个 MainActorQueue 的任务可以等待队列结果而不死锁。

MainActorQueue 可以轻松地以先入先出的顺序在非隔离环境中执行异步任务

@MainActor
func testMainActorQueueOrdering() async {
    @MainActor
    final class Counter {
        nonisolated
        func incrementAndAssertCountEquals(_ expectedCount: Int) {
            MainActorQueue.shared.enqueue {
                self.increment()
                let incrementedCount = self.count
                XCTAssertEqual(incrementedCount, expectedCount) // always succeeds
            }
        }

        func flushQueue() async {
            await MainActorQueue.shared.enqueueAndWait { }
        }

        func increment() {
            count += 1
        }

        var count = 0
    }

    let counter = Counter()
    for iteration in 1...100 {
        counter.incrementAndAssertCountEquals(iteration)
    }
    // Wait for all enqueued tasks to finish.
    await counter.flushQueue()
}

要求

  • Xcode 15.0 或更高版本。
  • iOS 13 或更高版本。
  • tvOS 13 或更高版本。
  • watchOS 6 或更高版本。
  • macOS 10.15 或更高版本。
  • Swift 5.9 或更高版本。

安装

Swift 包管理器

要在您的项目中使用Swift Package Manager安装swift-async-queue,可以将以下行添加到您的Package.swift文件中。

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

CocoaPods

使用CocoaPods在您的项目中安装swift-async-queue,请将以下内容添加到您的Podfile文件中。

platform :ios, '13.0'
pod 'AsyncQueue', '~> 0.5.0'

贡献

我很高兴您对swift-async-queue感兴趣,并期待看到您如何发展它。在提交拉取请求之前,请先阅读贡献指南

感谢,祝您排队顺利!

开发

双击仓库根目录下的Package.swift文件以在Xcode中打开项目。