Skip to content

Commit 4fb464f

Browse files
authored
Make FIFOQueue, ActorQueue, and tests pass strict concurrency checking (#13)
* Make FIFOQueue, ActorQueue, and tests pass strict concurrency checking * Post-merge review feedback from #9 & #12
1 parent 5e26f27 commit 4fb464f

File tree

7 files changed

+27
-22
lines changed

7 files changed

+27
-22
lines changed

AsyncQueue.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |s|
22
s.name = 'AsyncQueue'
3-
s.version = '0.2.0'
3+
s.version = '0.3.0'
44
s.license = 'MIT'
55
s.summary = 'A queue that enables ordered sending of events from synchronous to asynchronous code.'
66
s.homepage = 'https://github.com/dfed/swift-async-queue'

Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ let package = Package(
2222
dependencies: []),
2323
.testTarget(
2424
name: "AsyncQueueTests",
25-
dependencies: ["AsyncQueue"]),
25+
dependencies: ["AsyncQueue"],
26+
swiftSettings: [
27+
// TODO: Adopt `enableUpcomingFeature` once available.
28+
// https://github.com/apple/swift-evolution/blob/main/proposals/0362-piecemeal-future-features.md
29+
.unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"])
30+
]),
2631
]
2732
)

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,8 @@ FIFO execution has a key downside: the queue must wait for all previously enqueu
8989

9090
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.
9191

92-
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`:
93-
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.
92+
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`:
93+
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.
9494
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.
9595

9696
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
146146

147147
```swift
148148
dependencies: [
149-
.package(url: "https://github.com/dfed/swift-async-queue", from: "0.2.0"),
149+
.package(url: "https://github.com/dfed/swift-async-queue", from: "0.3.0"),
150150
]
151151
```
152152

@@ -156,7 +156,7 @@ To install swift-async-queue in your iOS project with [CocoaPods](http://cocoapo
156156

157157
```
158158
platform :ios, '13.0'
159-
pod 'AsyncQueue', '~> 0.2.0'
159+
pod 'AsyncQueue', '~> 0.3.0'
160160
```
161161

162162
## Contributing

Sources/AsyncQueue/ActorQueue.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@
5050
/// }
5151
/// ```
5252
///
53+
/// - 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.
5354
/// - Precondition: The lifecycle of an `ActorQueue` must not exceed that of the adopted actor.
54-
public final class ActorQueue<ActorType: Actor> {
55+
public final class ActorQueue<ActorType: Actor>: @unchecked Sendable {
5556

5657
// MARK: Initialization
5758

@@ -78,7 +79,7 @@ public final class ActorQueue<ActorType: Actor> {
7879

7980
// MARK: Public
8081

81-
/// Sets the actor context within which each `enqueue` and `enqueueAndWait` task will execute.
82+
/// Sets the actor context within which each `enqueue` and `enqueueAndWait`ed task will execute.
8283
/// It is recommended that this method be called in the adopted actor’s `init` method.
8384
/// **Must be called prior to enqueuing any work on the receiver.**
8485
///
@@ -100,7 +101,7 @@ public final class ActorQueue<ActorType: Actor> {
100101
/// The scheduled task will not execute until all prior tasks have completed or suspended.
101102
/// - Parameter task: The task to enqueue. The task's parameter is a reference to the actor whose execution context has been adopted.
102103
/// - Returns: The value returned from the enqueued task.
103-
public func enqueueAndWait<T>(_ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T {
104+
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T {
104105
let executionContext = self.executionContext // Capture/retain the executionContext before suspending.
105106
return await withUnsafeContinuation { continuation in
106107
taskStreamContinuation.yield(ActorTask(executionContext: executionContext) { executionContext in
@@ -113,7 +114,7 @@ public final class ActorQueue<ActorType: Actor> {
113114
/// The scheduled task will not execute until all prior tasks have completed or suspended.
114115
/// - Parameter task: The task to enqueue. The task's parameter is a reference to the actor whose execution context has been adopted.
115116
/// - Returns: The value returned from the enqueued task.
116-
public func enqueueAndWait<T>(_ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T {
117+
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T {
117118
let executionContext = self.executionContext // Capture/retain the executionContext before suspending.
118119
return try await withUnsafeThrowingContinuation { continuation in
119120
taskStreamContinuation.yield(ActorTask(executionContext: executionContext) { executionContext in
@@ -150,7 +151,6 @@ public final class ActorQueue<ActorType: Actor> {
150151
let executionContext: ActorType
151152
let task: @Sendable (isolated ActorType) async -> Void
152153
}
153-
154154
}
155155

156156
extension Actor {

Sources/AsyncQueue/FIFOQueue.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
/// A queue that executes asynchronous tasks enqueued from a nonisolated context in FIFO order.
2424
/// Tasks are guaranteed to begin _and end_ executing in the order in which they are enqueued.
25-
/// 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.
25+
/// 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.
2626
public final class FIFOQueue: Sendable {
2727

2828
// MARK: Initialization
@@ -71,7 +71,7 @@ public final class FIFOQueue: Sendable {
7171
/// The scheduled task will not execute until all prior tasks – including suspended tasks – have completed.
7272
/// - Parameter task: The task to enqueue.
7373
/// - Returns: The value returned from the enqueued task.
74-
public func enqueueAndWait<T>(_ task: @escaping @Sendable () async -> T) async -> T {
74+
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async -> T) async -> T {
7575
await withUnsafeContinuation { continuation in
7676
taskStreamContinuation.yield {
7777
continuation.resume(returning: await task())
@@ -85,7 +85,7 @@ public final class FIFOQueue: Sendable {
8585
/// - isolatedActor: The actor within which the task is isolated.
8686
/// - task: The task to enqueue.
8787
/// - Returns: The value returned from the enqueued task.
88-
public func enqueueAndWait<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T {
88+
public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T {
8989
await withUnsafeContinuation { continuation in
9090
taskStreamContinuation.yield {
9191
continuation.resume(returning: await task(isolatedActor))
@@ -97,7 +97,7 @@ public final class FIFOQueue: Sendable {
9797
/// The scheduled task will not execute until all prior tasks – including suspended tasks – have completed.
9898
/// - Parameter task: The task to enqueue.
9999
/// - Returns: The value returned from the enqueued task.
100-
public func enqueueAndWait<T>(_ task: @escaping @Sendable () async throws -> T) async throws -> T {
100+
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async throws -> T) async throws -> T {
101101
try await withUnsafeThrowingContinuation { continuation in
102102
taskStreamContinuation.yield {
103103
do {
@@ -115,7 +115,7 @@ public final class FIFOQueue: Sendable {
115115
/// - isolatedActor: The actor within which the task is isolated.
116116
/// - task: The task to enqueue.
117117
/// - Returns: The value returned from the enqueued task.
118-
public func enqueueAndWait<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T {
118+
public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T {
119119
try await withUnsafeThrowingContinuation { continuation in
120120
taskStreamContinuation.yield {
121121
do {

Tests/AsyncQueueTests/ActorQueueTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,17 @@ final class ActorQueueTests: XCTestCase {
6565

6666
func test_enqueue_taskParameterIsAdoptedActor() async {
6767
let semaphore = Semaphore()
68-
systemUnderTest.enqueue { counter in
69-
XCTAssertTrue(counter === self.counter)
68+
systemUnderTest.enqueue { [storedCounter = counter] counter in
69+
XCTAssertTrue(counter === storedCounter)
7070
await semaphore.signal()
7171
}
7272

7373
await semaphore.wait()
7474
}
7575

7676
func test_enqueueAndWait_taskParameterIsAdoptedActor() async {
77-
await systemUnderTest.enqueueAndWait { counter in
78-
XCTAssertTrue(counter === self.counter)
77+
await systemUnderTest.enqueueAndWait { [storedCounter = counter] counter in
78+
XCTAssertTrue(counter === storedCounter)
7979
}
8080
}
8181

Tests/AsyncQueueTests/FIFOQueueTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ final class FIFOQueueTests: XCTestCase {
7878
systemUnderTest.enqueue {
7979
let isWaiting = await semaphore.isWaiting
8080
// This test will fail occasionally if we aren't executing atomically.
81-
// You can prove this to yourself by replacing `systemUnderTest.async` above with `Task`.
81+
// You can prove this to yourself by replacing `systemUnderTest.enqueue` above with `Task`.
8282
XCTAssertFalse(isWaiting)
8383
// Signal the semaphore before or after we wait – let the scheduler decide.
8484
Task {
@@ -97,7 +97,7 @@ final class FIFOQueueTests: XCTestCase {
9797
systemUnderTest.enqueue(on: semaphore) { semaphore in
9898
let isWaiting = semaphore.isWaiting
9999
// This test will fail occasionally if we aren't executing atomically.
100-
// You can prove this to yourself by replacing `systemUnderTest.async` above with `Task`.
100+
// You can prove this to yourself by replacing `systemUnderTest.enqueue` above with `Task`.
101101
XCTAssertFalse(isWaiting)
102102
// Signal the semaphore before or after we wait – let the scheduler decide.
103103
Task {

0 commit comments

Comments
 (0)