Swift Task Groups

Swift task groups (TaskGroup, ThrowingTaskGroup) are a feature introduced in Swift 5.5 that allow you to create and manage a collection of child tasks that run concurrently and independently of each other. You can use task groups to perform multiple asynchronous operations in parallel and wait for their results to be available.

To use task groups, you need to call the withTaskGroup(of:returning:body:) function and pass a closure that receives a task group as an argument. Inside the closure, you can add child tasks to the task group by calling the addTask(priority:operation:) method on the task group. Each child task is an asynchronous function that returns a value of the same type as the task group.

You can access the results of the child tasks by calling the next() method on the task group, which returns an optional value of the child task result type. The next() method waits for the next child task to complete and returns its value, or returns nil if there are no more tasks in the group. You can also use the waitForAll() method to wait for all the remaining tasks in the group to finish without returning any values.

Alternatively, you can use a task group as an asynchronous sequence by conforming its result type to Sendable. This allows you to iterate over the task group using a for await loop or use other methods from the AsyncSequence protocol, such as map, filter, reduce, etc.

Here is an example of using a task group to perform three slow divide operations in parallel and print their results:

// Define a struct that represents a slow divide operation
struct SlowDivideOperation {
    let name: String
    let a: Double
    let b: Double
    let sleepDuration: UInt64
    
    func execute() async -> Double {
        // Sleep for x seconds
        await Task.sleep(sleepDuration * 1_000_000_000)
        let value = a / b
        return value
    }
}

// Define an array of slow divide operations
let operations = [
    SlowDivideOperation(name: "Operation 1", a: 10, b: 2, sleepDuration: 3),
    SlowDivideOperation(name: "Operation 2", a: 20, b: 4, sleepDuration: 2),
    SlowDivideOperation(name: "Operation 3", a: 30, b: 6, sleepDuration: 1)
]

// Create a task group that returns Double values
await withTaskGroup(of: Double.self) { group in
    // Add each operation as a child task to the group
    for operation in operations {
        print("Adding \(operation.name) to the group")
        group.addTask {
            // Execute the operation and return its value
            let value = await operation.execute()
            print("\(operation.name) finished with value \(value)")
            return value
        }
    }
    
    // Iterate over the group as an async sequence and print each result
    for await result in group {
        print("Result from the group: \(result)")
    }
    
    // Alternatively, use next() method to get each result
//     while let result = await group.next() {
//         print("Result from the group: \(result)")
//     }
}

The output of this code is:

Adding Operation 1 to the group
Adding Operation 2 to the group
Adding Operation 3 to the group
Operation 3 finished with value 5.0
Result from the group: 5.0
Operation 2 finished with value 5.0
Result from the group: 5.0
Operation 1 finished with value 5.0
Result from the group: 5.0

As you can see, the operations run concurrently and finish in different order depending on their sleep duration. The task group returns each result as soon as it is available and does not block the main thread.