print("Starting await...")
await withTaskGroup(of: Void.self) { taskGroup in
/* add some tasks. and then... */
taskGroup.cancelAll()
}
print("Returned from await") // doesn't print until after my tasks actually finish
I am surprised that I don't reach the "returned from await" state after canceling my tasks, right away: is the task group waiting for my tasks to actually co-operate by ending, before letting me get to my final print?
In my case, the tasks will eventually respond to the cancelation, but not right away. I am surprised that after I have canceled the tasks, I still have to wait for that outer "await" to finish up.
I feel like I'm missing something big and obvious.
And now the question morphs into: "OK, I canceled one of my started tasks, and that task is blocked waiting on a checkedContinuation. So why didn't it return from await checkedContinuation when I explicitly canceled its task, using taskGroup.cancelAll()?"
And now the question morphs into: "OK, I canceled one of my started tasks, and that task is blocked waiting on a checkedContinuation. So why didn't it return from await checkedContinuation when I explicitly canceled its task, using taskGroup.cancelAll()?"
Because cancellation is cooperative, i.e. a Task always requires whatever it is executing to explicitly check for cancellation, unwind its stack and return before CancellationError() can be thrown by the continuation.
Depending on your use case, you may need to wrap your continuation with withTaskCancellationHandler() so that you can signal your subsystem to stop what it's doing and return early.
That sounds like a good answer, and I will try it. However, there might be a simpler reason this isn't working:
await withTaskGroup(of: Void.self) { taskGroup in
/* add some tasks. and then... */
taskGroup.addTask {
await someFunction()
}
taskGroup.cancelAll()
}
the code "someFunction()" ends up launching its own tasks, and has to wait for them. I thought taskGroup.cancelAll would propagate down to the Task's spawned by someFunction(), but now I think they won't. Not actually sure how to design around this now.
Cancellation does propagate to child tasks, it’s just that they also must react cooperatively. There’s no “interrupt” per se, other than the cancellation handlers which also are just another way to cooperatively react.
By "launching its own tasks", do you mean invoking Task { [...] }? These tasks are unstructured and do not have a parent/child relationship with someFunction(); The cancellation will not propagate.
Only TaskGroups and async let constructs create proper "child tasks" whose execution context and lifecycle remain attached to the caller.
Yes, that's exactly what I meant. OK, I fell down the unstructured concurrency hole, but given the modular nature of what I'm doing, I don't think I can do anything else.
Before I file any issues, what is the most correct place to view documentation? For example, I googled withTaskCancellationHandler() and came up with several Apple pages that listed the function without even a hint of what it did.
I'd prefer not to file issues about the wrong version of documentation...
So the suggestion "use withTaskCancellationHandler()" was exactly the right answer to my question, of how to bridge across my unstructured concurrency divide.
Swift forums rocks. Thanks for the ultra speed responses/help, all.