I have a couple assumptions about what I am trying to achieve here (which I think are confirmed by either documentation or forum posts):
withUnsafeContinuation
synchronously executes its body, without introducing any suspension points before running it. From the docs:
The body of the closure executes synchronously on the calling task, and once it returns the calling task is suspended. It is possible to immediately resume the task, or escape the continuation in order to complete it afterwards, which will then resume the suspended task.
withTaskCancellationHandler
doesn't suspend or change isolation unless the closure contained does. From the docs:
The
operation
closure executes on the calling execution context, and doesn’t suspend or change execution context unless code contained within the closure does so. In other words, the potential suspension point of thewithTaskCancellationHandler(operation:onCancel:)
never suspends by itself before executing the operation.
- When there is no switch in isolation, there is no suspension point. Was confirmed in this post: Suspension guarantees when isolation does not change
@isolated(any)
and thereforeTaskGroup.addTask(_:)
+ isolated closure guarantees that work is scheduled in order of callingaddTask
. Kind of mentioned in the proposal: swift-evolution/proposals/0431-isolated-any-functions.md at main · swiftlang/swift-evolution · GitHub
However, I recently wrote something similar to this:
@Test
func testSomething() async {
let lock = OSAllocatedUnfairLock(initialState: 0)
func shouldNotSuspend(
value: Int,
isolation: isolated (any Actor)? = #isolation
) async {
await withTaskCancellationHandler {
await withUnsafeContinuation { continuation in
lock.withLock { $0 = value }
continuation.resume()
}
} onCancel: { }
}
await withTaskGroup(of: Void.self) { taskGroup in
taskGroup.addTask { @Sendable @MainActor in
await shouldNotSuspend(value: 1)
}
taskGroup.addTask { @Sendable @MainActor in
await shouldNotSuspend(value: 2)
}
await taskGroup.waitForAll()
}
lock.withLock { value in
#expect(value == 2)
}
}
If you run this test a couple of times (I mostly just do 100 runs), it will fail at some point, saying that value is 1
instead of 2
. Now, the title mentions that the problem seems to be withTaskCancellationHandler
. This is because, if you'd remove withTaskCancellationHandler
and just keep withUnsafeContinuation
, you can run the test 1000 times and it will not fail.
Would someone mind explaining, what I could be doing wrong? Thank you.