diff --git a/AsyncQueue.podspec b/AsyncQueue.podspec index d128ed1..8bc2970 100644 --- a/AsyncQueue.podspec +++ b/AsyncQueue.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'AsyncQueue' - s.version = '0.2.0' + s.version = '0.3.0' s.license = 'MIT' s.summary = 'A queue that enables ordered sending of events from synchronous to asynchronous code.' s.homepage = 'https://github.com/dfed/swift-async-queue' diff --git a/Package.swift b/Package.swift index a832530..0c689a4 100644 --- a/Package.swift +++ b/Package.swift @@ -22,6 +22,11 @@ let package = Package( dependencies: []), .testTarget( name: "AsyncQueueTests", - dependencies: ["AsyncQueue"]), + dependencies: ["AsyncQueue"], + swiftSettings: [ + // TODO: Adopt `enableUpcomingFeature` once available. + // https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md + .unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"]) + ]), ] ) diff --git a/README.md b/README.md index daecbb6..2148892 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,8 @@ FIFO execution has a key downside: the queue must wait for all previously enqueu 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 the 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 few 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 `let` constant on the adopted `actor`. Enqueuing a task to an `ActorQueue` isntance after its adopted `actor` has been deallocated will result in a crash. +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: @@ -146,7 +146,7 @@ To install swift-async-queue in your iOS project with [Swift Package Manager](ht ```swift dependencies: [ - .package(url: "https://github.com/dfed/swift-async-queue", from: "0.2.0"), + .package(url: "https://github.com/dfed/swift-async-queue", from: "0.3.0"), ] ``` @@ -156,7 +156,7 @@ To install swift-async-queue in your iOS project with [CocoaPods](http://cocoapo ``` platform :ios, '13.0' -pod 'AsyncQueue', '~> 0.2.0' +pod 'AsyncQueue', '~> 0.3.0' ``` ## Contributing diff --git a/Sources/AsyncQueue/ActorQueue.swift b/Sources/AsyncQueue/ActorQueue.swift index 9c8b6b7..dc00de4 100644 --- a/Sources/AsyncQueue/ActorQueue.swift +++ b/Sources/AsyncQueue/ActorQueue.swift @@ -50,8 +50,9 @@ /// } /// ``` /// +/// - Warning: The `ActorQueue`'s conformance to `@unchecked Sendable` is safe if and only if `adoptExecutionContext(of:)` is called only from the adopted actor's `init` method. /// - Precondition: The lifecycle of an `ActorQueue` must not exceed that of the adopted actor. -public final class ActorQueue { +public final class ActorQueue: @unchecked Sendable { // MARK: Initialization @@ -78,7 +79,7 @@ public final class ActorQueue { // MARK: Public - /// Sets the actor context within which each `enqueue` and `enqueueAndWait` task will execute. + /// Sets the actor context within which each `enqueue` and `enqueueAndWait`ed task will execute. /// It is recommended that this method be called in the adopted actor’s `init` method. /// **Must be called prior to enqueuing any work on the receiver.** /// @@ -100,7 +101,7 @@ public final class ActorQueue { /// The scheduled task will not execute until all prior tasks have completed or suspended. /// - Parameter task: The task to enqueue. The task's parameter is a reference to the actor whose execution context has been adopted. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(_ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { + public func enqueueAndWait(_ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { let executionContext = self.executionContext // Capture/retain the executionContext before suspending. return await withUnsafeContinuation { continuation in taskStreamContinuation.yield(ActorTask(executionContext: executionContext) { executionContext in @@ -113,7 +114,7 @@ public final class ActorQueue { /// The scheduled task will not execute until all prior tasks have completed or suspended. /// - Parameter task: The task to enqueue. The task's parameter is a reference to the actor whose execution context has been adopted. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(_ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { + public func enqueueAndWait(_ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { let executionContext = self.executionContext // Capture/retain the executionContext before suspending. return try await withUnsafeThrowingContinuation { continuation in taskStreamContinuation.yield(ActorTask(executionContext: executionContext) { executionContext in @@ -150,7 +151,6 @@ public final class ActorQueue { let executionContext: ActorType let task: @Sendable (isolated ActorType) async -> Void } - } extension Actor { diff --git a/Sources/AsyncQueue/FIFOQueue.swift b/Sources/AsyncQueue/FIFOQueue.swift index 401503e..384e53f 100644 --- a/Sources/AsyncQueue/FIFOQueue.swift +++ b/Sources/AsyncQueue/FIFOQueue.swift @@ -22,7 +22,7 @@ /// A queue that executes asynchronous tasks enqueued from a nonisolated context in FIFO order. /// Tasks are guaranteed to begin _and end_ executing in the order in which they are enqueued. -/// Asynchronous tasks sent to this queue work as they would in a `DispatchQueue` type. Attempting to `await` this queue from a task executing on this queue will result in a deadlock. +/// Asynchronous tasks sent to this queue work as they would in a `DispatchQueue` type. Attempting to `enqueueAndWait` this queue from a task executing on this queue will result in a deadlock. public final class FIFOQueue: Sendable { // MARK: Initialization @@ -71,7 +71,7 @@ public final class FIFOQueue: Sendable { /// The scheduled task will not execute until all prior tasks – including suspended tasks – have completed. /// - Parameter task: The task to enqueue. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(_ task: @escaping @Sendable () async -> T) async -> T { + public func enqueueAndWait(_ task: @escaping @Sendable () async -> T) async -> T { await withUnsafeContinuation { continuation in taskStreamContinuation.yield { continuation.resume(returning: await task()) @@ -85,7 +85,7 @@ public final class FIFOQueue: Sendable { /// - isolatedActor: The actor within which the task is isolated. /// - task: The task to enqueue. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { + public func enqueueAndWait(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { await withUnsafeContinuation { continuation in taskStreamContinuation.yield { continuation.resume(returning: await task(isolatedActor)) @@ -97,7 +97,7 @@ public final class FIFOQueue: Sendable { /// The scheduled task will not execute until all prior tasks – including suspended tasks – have completed. /// - Parameter task: The task to enqueue. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(_ task: @escaping @Sendable () async throws -> T) async throws -> T { + public func enqueueAndWait(_ task: @escaping @Sendable () async throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in taskStreamContinuation.yield { do { @@ -115,7 +115,7 @@ public final class FIFOQueue: Sendable { /// - isolatedActor: The actor within which the task is isolated. /// - task: The task to enqueue. /// - Returns: The value returned from the enqueued task. - public func enqueueAndWait(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { + public func enqueueAndWait(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { try await withUnsafeThrowingContinuation { continuation in taskStreamContinuation.yield { do { diff --git a/Tests/AsyncQueueTests/ActorQueueTests.swift b/Tests/AsyncQueueTests/ActorQueueTests.swift index d16c70e..5c8d250 100644 --- a/Tests/AsyncQueueTests/ActorQueueTests.swift +++ b/Tests/AsyncQueueTests/ActorQueueTests.swift @@ -65,8 +65,8 @@ final class ActorQueueTests: XCTestCase { func test_enqueue_taskParameterIsAdoptedActor() async { let semaphore = Semaphore() - systemUnderTest.enqueue { counter in - XCTAssertTrue(counter === self.counter) + systemUnderTest.enqueue { [storedCounter = counter] counter in + XCTAssertTrue(counter === storedCounter) await semaphore.signal() } @@ -74,8 +74,8 @@ final class ActorQueueTests: XCTestCase { } func test_enqueueAndWait_taskParameterIsAdoptedActor() async { - await systemUnderTest.enqueueAndWait { counter in - XCTAssertTrue(counter === self.counter) + await systemUnderTest.enqueueAndWait { [storedCounter = counter] counter in + XCTAssertTrue(counter === storedCounter) } } diff --git a/Tests/AsyncQueueTests/FIFOQueueTests.swift b/Tests/AsyncQueueTests/FIFOQueueTests.swift index 0e0d1bf..8799b9a 100644 --- a/Tests/AsyncQueueTests/FIFOQueueTests.swift +++ b/Tests/AsyncQueueTests/FIFOQueueTests.swift @@ -78,7 +78,7 @@ final class FIFOQueueTests: XCTestCase { systemUnderTest.enqueue { let isWaiting = await semaphore.isWaiting // This test will fail occasionally if we aren't executing atomically. - // You can prove this to yourself by replacing `systemUnderTest.async` above with `Task`. + // You can prove this to yourself by replacing `systemUnderTest.enqueue` above with `Task`. XCTAssertFalse(isWaiting) // Signal the semaphore before or after we wait – let the scheduler decide. Task { @@ -97,7 +97,7 @@ final class FIFOQueueTests: XCTestCase { systemUnderTest.enqueue(on: semaphore) { semaphore in let isWaiting = semaphore.isWaiting // This test will fail occasionally if we aren't executing atomically. - // You can prove this to yourself by replacing `systemUnderTest.async` above with `Task`. + // You can prove this to yourself by replacing `systemUnderTest.enqueue` above with `Task`. XCTAssertFalse(isWaiting) // Signal the semaphore before or after we wait – let the scheduler decide. Task {