swift-async-queue
一个队列库,允许将有序任务从同步环境发送到异步环境。
任务排序与 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 顺序执行来自非隔离上下文队列的异步任务。发送到这些队列的任务将保证按它们入队的顺序开始和结束执行。FIFOQueue 与 DispatchQueue 的执行方式类似:入队任务原子执行,如果在 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 时必须满足一些要求
- 任何
ActorQueue的生命周期不应超过其actor的生命周期。强烈建议将ActorQueue作为采用的actor上的private let常量。在其采用的actor被解除分配后,向ActorQueue实例入队任务会导致崩溃。 - 使用
ActorQueue的actor应在actor的init中将队列的采用的执行上下文设置为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中打开项目。