NB. As @Douglas_Gregor suggests, perhaps cancellation should be considered as part of Structured Concurrency. For that reason I cross-posted to that pitch topic.
I believe the *Continuation
API needs cancellation support. A wrapped callback-based API could be performing significant work, and it’d be wasteful to let that continue after the wrapping task is cancelled.
@Lantua already hinted at this yesterday, suggesting to expose the current Task.Handle
, but IIUC those are meant to control the task from the outside, whereas with this continuation API we’re really “inside” a task. Task.Handle
would expose the cancel
method (which cancels the task when called), whereas what we may need here is to define what happens after the task was cancelled.
I believe we need to be able to:
- check if the wrapping task was already cancelled;
- install a cancellation handler where we could instruct the wrapped API to cancel.
I propose adding isTaskCancelled
methods and taskCancellationHandler
variables. In order to be able to set the latter, the *Continuation
would have to be passed inout
to the closure passed to with*Continuation
. Using the simplest example from the pitch document for clarity:
// Continuation API (throwing & checked variants omitted)
struct UnsafeContinuation<T> {
func resume(returning: T)
func isTaskCancelled() -> Bool
var taskCancellationHandler: () -> Void = {} // Initially nop
}
func withUnsafeContinuation<T>(
_ operation: (inout UnsafeContinuation<T>) -> Void
) async -> T
// Example completion callback-based API with cancellation handle
struct OperationHandle {
func cancel()
}
func beginOperation(completion: (OperationResult) -> Void) -> OperationHandle
// Example
func operation() async -> OperationResult {
return await withUnsafeContinuation { continuation in
let operationHandle = beginOperation(completion: { result in
guard !continuation.isTaskCancelled() else { return }
continuation.resume(returning: result)
})
continuation.taskCancellationHandler = {
operationHandle.cancel()
}
}
}