[Concurrency] Continuations for interfacing async tasks with synchronous code

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()
		}
	}
}