The checks (both traps and warnings) should happen at all optimization levels. I can make this explicit in the proposal.
The queue from which resume
is invoked is irrelevant, since it only transitions the task out of the suspended state; tasks already know what executor they're associated with, and will be scheduled on whatever queue/runloop/thread/other scheduling mechanism their current executor uses. If an API takes a queue argument on which to run its completion handler, it's probably best to pass the queue that the code dispatching the completion handler will run on, if you know it. In cases when we know a task's work can be safely enqueued on a specific dispatch, to allow the queue hops to be minimized, we could perhaps provide some API on Task
to get that queue for the current task, and have an unsafeResumeImmediately
variant of API on *Continuation
that immediately resumes execution of the task on the current thread, relying on the assumption that the code invoking resume
was already executed by the correct queue. This would be a very sharp knife, though, and using it incorrectly could lead to really subtle problems that'd be tough to diagnose, so I think it'd be good to take a wait-and-see approach to see if queue-hopping becomes a bottleneck in practice. Despite the API recommendations, nearly all of the completion-handler-based APIs in Apple's frameworks do not take a queue argument, and there's not a lot of consistency in how completion handlers are invoked, so very few Apple APIs would be able to take advantage of such an optimization today. And as more code adopts Swift's native tasks and async functions, the Swift compiler and runtime's own optimizations for avoiding expensive queue hops will hopefully reduce overall overhead in time.
As currently implemented, UnsafeContinuation
is a plain pointer to the task structured to be resumed, whereas CheckedContinuation
allocates a class instance to hold the task pointer, so compared to UnsafeContinuation
, it'll have ARC operations applied to it when copied around, and will use atomic operations to "take" the task pointer from the instance the first time it's resumed so that the double-resume protection is thread safe. When we have move-only types, we should be able to make a safe
Continuation
type that has to be resume
-d in order to dispose of it; before then, I don't see an obvious way to avoid the overhead in the language today. However, even with move-only types, since a lot of the
value of this API is interop with existing C and ObjC APIs, I wonder whether a move-only continuation type would be very practical to use, or just force the use of unsafe escape hatches down a layer.
I can reword the proposal to make this clearer. The distinction that we're trying to make is between resume
happening after the with*Continuation
block has finished executing and the task has suspended, and resume
happening while the with*Continuation
block is still executing. An example of the latter would be:
await withUnsafeContinuation { c in
if condition {
// Suspend the task and resume it later
doSomethingAsynchronously(completion: { c.resume(returning: ()) })
} else {
// Resume the task immediately
c.resume(returning: ())
// According to rule 2, we can't do anything after `resume` if we run
// it immediately
}
}
I'll amend the text to make this clearer.
The error propagation from with*Continuation
is either-or; if you specified a more specific Error type, it would have no type system impact on what the caller sees when withUnsafeThrowingContinuation
throws
. Task.Handle
's error parameterization seems a bit suspect to me for this reason too; in practice it can only ever be Never
or Error
.
It's a leak, and potentially a deadlock if other work relies on the abandoned task finishing, but it's not a memory safety violation. If the task is never resumed, it'll sit around forever holding onto whatever resources it was using when it suspended. My bigger concern about making leaking a task trap is the lack of determinism in where that trap happens; one could miss a trap because naive -Onone ARC put off destroying the checked continuation wrapper until the process ends for other reasons, and then end up shipping a -O build that cuts the lifetime shorter and exposes the trap.