Grupuri de Sarcini Asincrone în Swift: withTaskGroup și withThrowingTaskGroup în Swift (iOS & macOS)

Ghid aprofundat pentru concurența structurată dinamică în Swift: utilizarea withTaskGroup și withThrowingTaskGroup pentru execuția paralelă a colecțiilor de sarcini, anulare automată cooperativă și throttling de resurse.

Sintaxă Swift
await withTaskGroup(of: T.self) { group in group.addTask { ... } }

În timp ce directiva async let este excelentă pentru a rula un număr fix și cunoscut în avans de sarcini paralele, aplicațiile reale necesită frecvent procesarea unei colecții dinamice de elemente (descărcarea a zeci de imagini, interogarea a numeroase microservicii paralele). Pentru aceste scenarii, Swift oferă Task Groups prin funcțiile withTaskGroup și withThrowingTaskGroup.

Grupul de sarcini respectă principiile fundamentale ale concurenței structurate (Structured Concurrency): niciun task copil nu poate supraviețui domeniului funcției părinte, iar dacă un task aruncă o eroare, celelalte sarcini active sunt marcate automat pentru anulare (cancellation propagation).

Descărcarea Paralelă a Resurselor cu withThrowingTaskGroup
import Foundation

struct ImagineRezultat: Sendable {
    let id: Int
    let dimensiuneOcteti: Int
}

func descarcaToateImaginile(idUri: [Int]) async throws -> [ImagineRezultat] {
    // Inițializăm un grup structurat ce returnează instanțe de tip ImagineRezultat:
    try await withThrowingTaskGroup(of: ImagineRezultat.self) { group in
        for id in idUri {
            // Adăugăm dinamic câte o sarcină paralelă în grup:
            group.addTask {
                // Verificăm dacă sarcina nu a fost deja anulată cooperativ:
                try Task.checkCancellation()
                
                let url = URL(string: "https://images.dexblog.ro/img_\(id).jpg")!
                let (data, _) = try await URLSession.shared.data(from: url)
                
                return ImagineRezultat(id: id, dimensiuneOcteti: data.count)
            }
        }
        
        // Colectăm rezultatele asincron pe măsură ce sarcinile se termină:
        var rezultate: [ImagineRezultat] = []
        rezultate.reserveCapacity(idUri.count)
        
        for try await imagine in group {
            rezultate.append(imagine)
        }
        
        return rezultate
    }
}

Pattern de Throttling (Limitarea Concomitenței)

Pentru a evita epuizarea socket-urilor de rețea sau a memoriei când procesați mii de elemente, puteți limita numărul de sarcini active la o fereastră fixă (ex: maximum 4 în paralel):

try await withThrowingTaskGroup(of: Void.self) { group in
    let maxConcurent = 4
    var indexCurent = 0
    
    // Lansăm primele N sarcini:
    while indexCurent < idUri.count && indexCurent < maxConcurent {
        let item = idUri[indexCurent]
        group.addTask { try await proceseazaItem(item) }
        indexCurent += 1
    }
    
    // Pe măsură ce un task se finalizează, lansăm următorul item din coadă:
    for try await _ in group {
        if indexCurent < idUri.count {
            let item = idUri[indexCurent]
            group.addTask { try await proceseazaItem(item) }
            indexCurent += 1
        }
    }
}