Questions about TaskGroup.cancelAll

I am trying to use the TaskGroup feature in Swift 5.5. I would like to be able to manually cancel all subtasks as explained in the documentation.

I am using the code below and when I call cancelAll in withTaskGroup, everything works as expected and the uncompleted tasks are detected as isCancelled == true. However when I try to call it outside of withTaskGroup, I am unable to cancel taskgroup.

There is no problem if the cancel is done only for a single task (by using async).

Is there a problem with the cancel operation I perform outside of withTaskGroup? Or is it that taskgroup does not allow cancellation from outside.

@main
class Main{

    static var taskGroup:TaskGroup<Void>? = nil
    static var taskHandler:Task.Handle<Void,Never>?
    
    static func main() async {
        //Cancel TaskGroup Test
        
        await withTaskGroup(of: Void.self){ group in
            taskGroup = group
            group.async {
                print(await delayNumber())
            }
            /*
            taskGroup?.cancelAll()  // works. in withTaskGroup everything is woking fine.
            */
        }
        
        taskGroup?.cancelAll() // not work
        //taskGroup = nil // not work too
        
        
        //Cancel single Task Test
        taskHandler = async {
            print(await delayNumber())
        }
        
//        taskHandler?.cancel() // worked
    
        //Task.sleep(10 * 1_000_000_000) // wait to finish
        Thread.sleep(forTimeInterval: 10)
    }
    
    /*
     Task.sleep now has a problem that can cause the code to crash. So some useless calculations are done to consume time
     */
    static func delayNumber () async -> String{
        for i in 0..<1_000_000_0{
            if Task.isCancelled {
                return "cancelled"
            }
            _ = Int.max - i
        }
        return String(Int.random(in: 0...1000))
    }
}

As with other with APIs, you are *NOT* supposed to escape the closure parameter, i.e., group should not outlive the closure. This code would result in undefined behaviour.

Also, under normal circumstances, child tasks created from the task group will be awaited at the end of the closure, so there's nothing to cancel beyond that point.

1 Like

Thank you for your reply.
It looks like I need to add another async outside of withTaskGroup to complete the control of taskGroup.

ps:The code is only for testing, I just want to understand the mechanism of cancellation

@main
class Main{

    static var taskGroup:Task.Handle<Void,Never>? = nil // root task
    static var taskHandler:Task.Handle<Void,Never>? = nil
    
    static func main() async {
        //Cancel TaskGroup Test
        
        taskGroup = async { // root task
        await withTaskGroup(of: Void.self){ group in
            group.async {
                print(await delayNumber())
            }
            /*
            taskGroup?.cancelAll()  // works. in withTaskGroup everything is woking fine.
            */
        }
        }
        
        Thread.sleep(forTimeInterval: 3)
        
        taskGroup?.cancel() // cancel root task will cancel all sub task
        
        
        //Cancel single Task Test
        taskHandler = async {
            print(await delayNumber())
        }
        
//        taskHandler?.cancel() // worked
    
        //Task.sleep(10 * 1_000_000_000) // wait to finish
        Thread.sleep(forTimeInterval: 10)
    }
    
    /*
     Task.sleep now has a problem that can cause the code to crash. So some useless calculations are done to consume time
     */
    static func delayNumber () async -> String{
        for i in 0..<1_000_000_0{
            if Task.isCancelled {
                return "cancelled"
            }
            _ = Int.max - i
        }
        return String(Int.random(in: 0...1000))
    }
}

You might find the WWDC video below useful. The semantic of cancellation is just that "when a task is cancelled, so do all of its child tasks". So the important part is only to realize the relationship between each tasks, and figure out the implicit cancellation (if any) in your code.

1 Like
func fetchDataWithTimeout() async throws {
    return try await withCheckedThrowingContinuation { continuation in
        Task {
            try await withThrowingTaskGroup(of: Void.self) { group in
                // Task to fetch data
                group.addTask {
                    
                    try await self.fetchData()
                    return
                }
                // Task to enforce timeout
                group.addTask {
                    try await Task.sleep(nanoseconds: UInt64(2 * 1_000_000_000))
                    throw NSError(domain: "1", code: 1)
                }
                // Wait for the first task to complete
                do {
                    try await group.next()
                    // Cancel remaining tasks
                    group.cancelAll()
                    continuation.resume()
                    return
                    
                } catch {
                    group.cancelAll()
                    continuation.resume(throwing: error)
                    throw error
                }
            }
        }
    }
}

func fetchData() async throws {
    return await withCheckedContinuation { continuation in
        DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
            continuation.resume()
        }
    }
    
}