-
-
Notifications
You must be signed in to change notification settings - Fork 14
Make FIFOQueue, ActorQueue, and tests pass strict concurrency checking #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,11 @@ let package = Package( | |
| dependencies: []), | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we'd force the vended target to conform to include
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK I see that we use unsafe build flags in the test target and that's OK since it's not being consumed by other packages. |
||
| .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"]) | ||
| ]), | ||
| ] | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<ActorType: Actor> { | ||
| public final class ActorQueue<ActorType: Actor>: @unchecked Sendable { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we don't make This error makes sense: we're accessing a non-sendable
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We also can't make this type
The |
||
|
|
||
| // MARK: Initialization | ||
|
|
||
|
|
@@ -78,7 +79,7 @@ public final class ActorQueue<ActorType: Actor> { | |
|
|
||
| // 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<ActorType: Actor> { | |
| /// 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<T>(_ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | ||
| public func enqueueAndWait<T: Sendable>(_ 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<ActorType: Actor> { | |
| /// 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<T>(_ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { | ||
| public func enqueueAndWait<T: Sendable>(_ 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<ActorType: Actor> { | |
| let executionContext: ActorType | ||
| let task: @Sendable (isolated ActorType) async -> Void | ||
| } | ||
|
|
||
| } | ||
|
|
||
| extension Actor { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<T>(_ task: @escaping @Sendable () async -> T) async -> T { | ||
| public func enqueueAndWait<T: Sendable>(_ 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<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | ||
| public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async -> T) async -> T { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This This warning makes sense:
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To make sure I follow, we got a warning in all cases except the ones where the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm following correctly I think that these two methods on public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async -> T) async -> T
public func enqueueAndWait<T: Sendable>(_ task: @escaping @Sendable () async throws -> T) async throws -> TIs that right?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct!
My best guess is that this is a compiler bug. In all the methods I marked we're crossing a task boundary: from the task that created the async for loop to the call-site. But only in the methods where we have an
That is absolutely right. |
||
| 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<T>(_ task: @escaping @Sendable () async throws -> T) async throws -> T { | ||
| public func enqueueAndWait<T: Sendable>(_ 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<ActorType: Actor, T>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { | ||
| public func enqueueAndWait<ActorType: Actor, T: Sendable>(on isolatedActor: isolated ActorType, _ task: @escaping @Sendable (isolated ActorType) async throws -> T) async throws -> T { | ||
| try await withUnsafeThrowingContinuation { continuation in | ||
| taskStreamContinuation.yield { | ||
| do { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,17 +65,17 @@ final class ActorQueueTests: XCTestCase { | |
|
|
||
| func test_enqueue_taskParameterIsAdoptedActor() async { | ||
| let semaphore = Semaphore() | ||
| systemUnderTest.enqueue { counter in | ||
| XCTAssertTrue(counter === self.counter) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. capturing non-sendable |
||
| systemUnderTest.enqueue { [storedCounter = counter] counter in | ||
| XCTAssertTrue(counter === storedCounter) | ||
| await semaphore.signal() | ||
| } | ||
|
|
||
| await semaphore.wait() | ||
| } | ||
|
|
||
| func test_enqueueAndWait_taskParameterIsAdoptedActor() async { | ||
| await systemUnderTest.enqueueAndWait { counter in | ||
| XCTAssertTrue(counter === self.counter) | ||
| await systemUnderTest.enqueueAndWait { [storedCounter = counter] counter in | ||
| XCTAssertTrue(counter === storedCounter) | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately this PR represents a (reasonably small) breaking API change.