|
1 |
| -// REQUIRES: rdar145735542 |
2 | 1 | // RUN: %empty-directory(%t)
|
3 | 2 | // RUN: %target-build-swift -Xfrontend -disable-availability-checking %s %import-libdispatch -swift-version 6 -o %t/a.out
|
4 | 3 | // RUN: %target-codesign %t/a.out
|
@@ -62,6 +61,42 @@ extension ThreadID: @unchecked Sendable {}
|
62 | 61 | @globalActor
|
63 | 62 | actor MyGlobalActor {
|
64 | 63 | static let shared: MyGlobalActor = MyGlobalActor()
|
| 64 | + |
| 65 | + @MyGlobalActor |
| 66 | + static func test() {} |
| 67 | +} |
| 68 | + |
| 69 | +final class NaiveQueueExecutor: SerialExecutor { |
| 70 | + let queue: DispatchQueue |
| 71 | + |
| 72 | + init(queue: DispatchQueue) { |
| 73 | + self.queue = queue |
| 74 | + } |
| 75 | + |
| 76 | + public func enqueue(_ job: consuming ExecutorJob) { |
| 77 | + let unowned = UnownedJob(job) |
| 78 | + print("NaiveQueueExecutor(\(self.queue.label)) enqueue [thread:\(getCurrentThreadID())]") |
| 79 | + queue.async { |
| 80 | + unowned.runSynchronously(on: self.asUnownedSerialExecutor()) |
| 81 | + } |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +@globalActor |
| 86 | +actor DifferentGlobalActor { |
| 87 | + static let queue = DispatchQueue(label: "DifferentGlobalActor-queue") |
| 88 | + let executor: NaiveQueueExecutor |
| 89 | + nonisolated let unownedExecutor: UnownedSerialExecutor |
| 90 | + |
| 91 | + init() { |
| 92 | + self.executor = NaiveQueueExecutor(queue: DifferentGlobalActor.queue) |
| 93 | + self.unownedExecutor = executor.asUnownedSerialExecutor() |
| 94 | + } |
| 95 | + |
| 96 | + static let shared: DifferentGlobalActor = DifferentGlobalActor() |
| 97 | + |
| 98 | + @DifferentGlobalActor |
| 99 | + static func test() {} |
65 | 100 | }
|
66 | 101 |
|
67 | 102 | // Test on all platforms
|
@@ -94,6 +129,49 @@ func syncOnMyGlobalActor() -> [Task<Void, Never>] {
|
94 | 129 | return [t1, tt]
|
95 | 130 | }
|
96 | 131 |
|
| 132 | +func syncOnMyGlobalActorHopToDifferentActor() -> [Task<Void, Never>] { |
| 133 | + MyGlobalActor.shared.preconditionIsolated("Should be executing on the global actor here") |
| 134 | + print("Confirmed to be on @MyGlobalActor") |
| 135 | + |
| 136 | + // This task must be guaranteed to happen AFTER 'tt' because we are already on this actor |
| 137 | + // so this enqueue must happen after we give up the actor. |
| 138 | + print("schedule Task { @DifferentGlobalActor }, before startSynchronously [thread:\(getCurrentThreadID())] @ :\(#line)") |
| 139 | + let t1 = Task { @DifferentGlobalActor in |
| 140 | + print("inside Task { @DifferentGlobalActor } [thread:\(getCurrentThreadID())] @ :\(#line)") |
| 141 | + DifferentGlobalActor.shared.preconditionIsolated("Expected Task{} to be on DifferentGlobalActor") |
| 142 | + } |
| 143 | + |
| 144 | + print("before startSynchronously [thread:\(getCurrentThreadID())] @ :\(#line)") |
| 145 | + let outerTID = getCurrentThreadID() |
| 146 | + let tt = Task.startSynchronously { @DifferentGlobalActor in |
| 147 | + let innerTID = getCurrentThreadID() |
| 148 | + print("inside startSynchronously, outer thread = \(outerTID)") |
| 149 | + print("inside startSynchronously, inner thread = \(innerTID)") |
| 150 | + if (compareThreadIDs(outerTID, .equal, innerTID)) { |
| 151 | + // This case specifically is NOT synchronously run because we specified a different isolation for the closure |
| 152 | + // and FORCED a hop to the DifferentGlobalActor executor. |
| 153 | + print("ERROR! Outer Thread ID must NOT equal Thread ID inside runSynchronously synchronous part!") |
| 154 | + } |
| 155 | + // We crucially need to see this task be enqueued on the different global actor, |
| 156 | + // so it did not execute "synchronously" after all - it had to hop to the other actor. |
| 157 | + dispatchPrecondition(condition: .onQueue(DifferentGlobalActor.queue)) |
| 158 | + DifferentGlobalActor.shared.preconditionIsolated("Expected Task.startSynchronously { @DifferentGlobalActor in } to be on DifferentGlobalActor") |
| 159 | + |
| 160 | + print("inside startSynchronously, sleep now [thread:\(getCurrentThreadID())] @ :\(#line)") |
| 161 | + _ = try? await Task.sleep(for: .milliseconds(100)) |
| 162 | + |
| 163 | + print("inside startSynchronously, after sleep [thread:\(getCurrentThreadID())] @ :\(#line)") |
| 164 | + dispatchPrecondition(condition: .onQueue(DifferentGlobalActor.queue)) |
| 165 | + DifferentGlobalActor.shared.preconditionIsolated("Expected Task.startSynchronously { @DifferentGlobalActor in } to be on DifferentGlobalActor") |
| 166 | + |
| 167 | + // do something here |
| 168 | + await MyGlobalActor.test() |
| 169 | + DifferentGlobalActor.test() |
| 170 | + } |
| 171 | + |
| 172 | + return [t1, tt] |
| 173 | +} |
| 174 | + |
97 | 175 | func syncOnNonTaskThread(synchronousTask behavior: SynchronousTaskBehavior) {
|
98 | 176 | let sem1 = DispatchSemaphore(value: 0)
|
99 | 177 | let sem2 = DispatchSemaphore(value: 0)
|
@@ -162,6 +240,33 @@ await Task { @MyGlobalActor in
|
162 | 240 | // resume on some other thread
|
163 | 241 | // CHECK: after sleep, inside startSynchronously
|
164 | 242 |
|
| 243 | +print("\n\n==== ------------------------------------------------------------------") |
| 244 | +print("syncOnMyGlobalActorHopToDifferentActor()") |
| 245 | + |
| 246 | +await Task { @MyGlobalActor in |
| 247 | + MyGlobalActor.shared.preconditionIsolated("Should be executing on the global actor here") |
| 248 | + for t in syncOnMyGlobalActorHopToDifferentActor() { |
| 249 | + await t.value |
| 250 | + } |
| 251 | +}.value |
| 252 | + |
| 253 | +// Assertion Notes: We expect the task to be on the specified queue as we force the Task.startSynchronously |
| 254 | +// task to enqueue on the DifferentGlobalActor, however we CANNOT use threads to verify this behavior, |
| 255 | +// because dispatch may still pull tricks and reuse threads. We can only verify that we're on the right |
| 256 | +// queue, and that the `enqueue` calls on the target executor happen when we expect them to. |
| 257 | +// |
| 258 | +// CHECK: syncOnMyGlobalActorHopToDifferentActor() |
| 259 | +// CHECK: Confirmed to be on @MyGlobalActor |
| 260 | +// CHECK: before startSynchronously |
| 261 | + |
| 262 | +// This IS actually enqueueing on the target actor (not synchronous), as expected: |
| 263 | +// CHECK: NaiveQueueExecutor(DifferentGlobalActor-queue) enqueue |
| 264 | +// CHECK: inside startSynchronously, sleep now |
| 265 | + |
| 266 | +// After the sleep we get back onto the specified executor as expected |
| 267 | +// CHECK: NaiveQueueExecutor(DifferentGlobalActor-queue) enqueue |
| 268 | +// CHECK: inside startSynchronously, after sleep |
| 269 | + |
165 | 270 | print("\n\n==== ------------------------------------------------------------------")
|
166 | 271 | var behavior: SynchronousTaskBehavior = .suspend
|
167 | 272 | print("syncOnNonTaskThread(synchronousTask: \(behavior))")
|
@@ -347,23 +452,6 @@ callActorFromStartSynchronousTask(recipient: .recipientOnQueue(RecipientOnQueue(
|
347 | 452 | // CHECK-NOT: ERROR!
|
348 | 453 | // CHECK: inside startSynchronously, done
|
349 | 454 |
|
350 |
| -final class NaiveQueueExecutor: SerialExecutor { |
351 |
| - let queue: DispatchQueue |
352 |
| - |
353 |
| - init(queue: DispatchQueue) { |
354 |
| - self.queue = queue |
355 |
| - } |
356 |
| - |
357 |
| - public func enqueue(_ job: consuming ExecutorJob) { |
358 |
| - let unowned = UnownedJob(job) |
359 |
| - print("NaiveQueueExecutor(\(self.queue.label)) enqueue... [thread:\(getCurrentThreadID())]") |
360 |
| - queue.async { |
361 |
| - print("NaiveQueueExecutor(\(self.queue.label)) enqueue: run [thread:\(getCurrentThreadID())]") |
362 |
| - unowned.runSynchronously(on: self.asUnownedSerialExecutor()) |
363 |
| - } |
364 |
| - } |
365 |
| -} |
366 |
| - |
367 | 455 | actor RecipientOnQueue {
|
368 | 456 | let executor: NaiveQueueExecutor
|
369 | 457 | nonisolated let unownedExecutor: UnownedSerialExecutor
|
|
0 commit comments