TaskGroup.cancelAll()
is important when you write a task group that only needs some of its child tasks to finish in order to deliver a result. You should call cancelAll()
before returning the result to tell the outstanding child tasks to cancel.
Examples of such task groups:
- Running an async function with a timeout
- A
race
function that races two async functions and returns the result of the one that finishes first (see example below)
If you forget to call TaskGroup.cancelAll()
, the outstanding child tasks will run to completion even though their result will be ignored. It’s very easy to forget because forgetting the call will not change the behavior of your program, only its performance, making the bug hard to find.
@s-k brought this up during the Structured Concurrency review: SE-0304 (3rd review): Structured Concurrency - #5 by s-k
Note that TaskGroup
does cancel outstanding child tasks if it exits abnormally, i.e. by throwing.
Another example where the cancelAll()
call is missing is actually in SE-0317 async let:
func race(left: () async -> Int, right: () async -> Int) async -> Int {
await withTaskGroup(of: Int.self) { group in
group.async { left() }
group.async { right() }
return await group.next()! // !-safe, there is at-least one result to collect
}
}
This function will always wait for both child tasks to run to completion, even though it only needs the result of the first one to the finish line.