Does taskGroup.cancelAll() require active co-operation to finish properly?

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.

1 Like

Sigh. The documentation on "withDiscardingTaskGroup" is quite clear.

"A group waits for all of its child tasks to complete before it returns. Even cancelled tasks must run until completion before this function returns"

I assume the same caveat applies to "withTaskGroup", but nobody spelled it out in the documentation?

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.

3 Likes

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.

We did a talk specifically around the details of structured concurrency over here: Beyond the basics of structured concurrency - WWDC23 - Videos - Apple Developer

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.

1 Like

If you see some missing documentation that’s easy to contribute, please consider doing so or fling an issue on GitHub - swiftlang/swift: The Swift Programming Language, we continue to keep improving documentation.

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.

2 Likes

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.

I'm seeing if withCancelationHandler() fixes it.

Will do! I was not aware one could do this.

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.

3 Likes