Pour conserver les calculs Swift réussis lorsque des tâches sœurs échouent, interceptez l’erreur de chaque calcul dans sa tâche enfant et renvoyez une valeur de résultat. Cet exemple en ligne de commande sur Mac recueille tous les résultats, préserve l’ordre des entrées et conserve les valeurs réussies.

La tâche consiste en un petit calcul de lot synthétique : élever au carré une liste d’entiers non négatifs et indiquer quelles entrées ont réussi. Les entrées négatives et les dépassements de capacité des entiers produisent des échecs individuels. Toutes les données de calcul restent en mémoire ; le programme affiche ses résultats dans Terminal.

Pourquoi une erreur peut-elle interrompre la collecte des résultats ?

L’échec d’une tâche enfant avec une erreur n’annule pas immédiatement ses tâches sœurs. Le point déterminant est le corps du groupe de tâches : lorsqu’une erreur s’échappe de ce corps, Swift annule les autres tâches enfants. Apple documente cette distinction dans sa référence sur withThrowingTaskGroup(of:returning:isolation:body:).

Par conséquent, une boucle d’itération avec levée d’erreur et un bloc catch externe peuvent interrompre la collecte à la première erreur observée. Des valeurs précédentes peuvent déjà être stockées, mais la boucle ne reprend pas après ce catch. Les tâches enfants restantes sont annulées si l’erreur s’échappe du corps du groupe.

Pour des calculs indépendants, définissez explicitement la politique de gestion des échecs. L’exemple ci-dessous utilise un groupe de tâches sans levée d’erreur, dont les enfants renvoient une réussite, un échec ou une annulation. Le calcul lui-même continue à lever une erreur ; son enveloppe convertit cette erreur en valeur avant qu’elle n’atteigne le groupe.

Cette approche respecte la limite documentée par Swift : les enfants d’un groupe de tâches sans levée d’erreur ne peuvent pas propager d’erreurs levées ; ils doivent les gérer en interne ou représenter leur résultat sous forme de valeur de retour. Consultez la documentation TaskGroup d’Apple.

Définir ce que chaque entrée doit renvoyer

Le contrat du lot prévoit un résultat par position d’entrée. Un calcul réussi renvoie un carré entier exact. Un calcul rejeté renvoie une description de l’échec. Un calcul ignoré après détection d’une annulation renvoie un résultat d’annulation.

Situation Résultat Comportement du lot
Entrée non négative dont le carré tient dans Int64 Réussite contenant le carré Conserver la valeur
Entrée négative Échec : negativeInput Continuer à recueillir les autres résultats
Carré dépassant la plage de Int64 Échec : overflow Continuer à recueillir les autres résultats
Annulation détectée avant le calcul Annulé Conserver les résultats terminés et ignorer les travaux en attente

Le rejet des entrées négatives est une règle d’application à titre d’illustration ; les nombres négatifs peuvent être mis au carré en mathématiques. En dissociant cette règle du dépassement de capacité, l’exemple montre deux conditions d’échec distinctes.

Pour la multiplication, utilisez multipliedReportingOverflow(by:) et examinez son indicateur de dépassement de capacité. Apple précise que la valeur partielle renvoyée correspond au produit complet uniquement si cet indicateur est faux ; sinon, elle est tronquée. L’exemple écarte cette valeur tronquée et lève une erreur. Consultez Int64.multipliedReportingOverflow(by:).

Chaque résultat contient aussi l’index de son entrée. Swift ne garantit pas l’ordre de fin des tâches enfants ; il est donc impossible de préserver l’ordre initial en ajoutant simplement les valeurs terminées à un tableau. Le guide du langage décrit ce comportement de planification dans Concurrency.

Exécuter l’exemple complet sur votre Mac

Utilisez un Mac avec une chaîne d’outils Swift 6 ou ultérieure. Dans Terminal, exécutez swift --version pour vérifier la version du compilateur installé, comme le recommande le guide Getting Started de Swift.org. Si Swift n’est pas disponible, suivez le lien d’installation de ce guide avant de continuer.

Enregistrez le programme complet suivant sous le nom Calculations.swift dans un dossier de travail. Il importe Foundation pour définir une limite de concurrence initiale à partir du nombre de processeurs actifs.

import Foundation

enum CalculationError: Error {
    case negativeInput
    case overflow
}

enum Outcome: Sendable {
    case success(Int64)
    case failure(String)
    case cancelled
}

struct Row: Sendable {
    let index: Int
    let outcome: Outcome
}

func square(_ input: Int64) throws -> Int64 {
    guard input >= 0 else {
        throw CalculationError.negativeInput
    }
    let product = input.multipliedReportingOverflow(by: input)
    guard !product.overflow else {
        throw CalculationError.overflow
    }
    return product.partialValue
}

func evaluate(index: Int, input: Int64) -> Row {
    do {
        try Task.checkCancellation()
        let value = try square(input)
        return Row(index: index, outcome: .success(value))
    } catch is CancellationError {
        return Row(index: index, outcome: .cancelled)
    } catch {
        return Row(
            index: index,
            outcome: .failure(String(describing: error))
        )
    }
}

func collect(
    _ inputs: [Int64],
    maxConcurrent: Int
) async -> [Row] {
    guard !inputs.isEmpty else { return [] }
    let width = min(inputs.count, max(1, maxConcurrent))

    return await withTaskGroup(of: Row.self) { group in
        var rows: [Row] = []
        rows.reserveCapacity(inputs.count)

        for index in 0..<width {
            let input = inputs[index]
            group.addTask {
                evaluate(index: index, input: input)
            }
        }
        var nextIndex = width

        while let row = await group.next() {
            rows.append(row)

            if Task.isCancelled {
                while nextIndex < inputs.count {
                    rows.append(Row(
                        index: nextIndex,
                        outcome: .cancelled
                    ))
                    nextIndex += 1
                }
            } else if nextIndex < inputs.count {
                let index = nextIndex
                let input = inputs[index]
                nextIndex += 1
                group.addTask {
                    evaluate(index: index, input: input)
                }
            }
        }

        return rows.sorted { $0.index < $1.index }
    }
}

@main
struct CalculationDemo {
    static func main() async {
        let inputs: [Int64] = [3, -2, 4, Int64.max, 0]
        let width = min(
            4,
            max(1, ProcessInfo.processInfo.activeProcessorCount)
        )
        let rows = await collect(inputs, maxConcurrent: width)
        var successes: [Int64] = []

        for row in rows {
            let input = inputs[row.index]
            switch row.outcome {
            case .success(let value):
                successes.append(value)
                print("[\(row.index)] \(input): success \(value)")
            case .failure(let message):
                print("[\(row.index)] \(input): failure \(message)")
            case .cancelled:
                print("[\(row.index)] \(input): cancelled")
            }
        }
        print("Successful values: \(successes)")
    }
}

Depuis ce dossier, compilez et exécutez le programme :

swiftc -swift-version 6 -parse-as-library Calculations.swift -o Calculations
./Calculations

L’option -parse-as-library permet à ce fichier source unique d’utiliser son point d’entrée explicite @main. L’exécutable effectue les calculs sans lire ni écrire de données d’application. Swift n’était pas disponible dans l’environnement de recherche ; le programme y a donc été examiné, mais pas compilé ni exécuté.

Pour les entrées fournies sans annulation, le résultat attendu est :

[0] 3: success 9
[1] -2: failure negativeInput
[2] 4: success 16
[3] 9223372036854775807: failure overflow
[4] 0: success 0
Successful values: [9, 16, 0]

L’ordre affiché est obtenu en triant par index d’entrée après la collecte. L’ordre d’exécution peut varier sans modifier cette présentation. Le tableau des valeurs réussies est créé ensuite, ce qui laisse le rapport complet des résultats disponible pour examen.

Comment le collecteur conserve-t-il chaque résultat réussi ?

La tâche parente possède le tableau des résultats. Chaque tâche enfant capture un index et une entrée immuable, calcule son résultat et renvoie une ligne. Les tâches enfants n’ajoutent jamais directement des éléments au tableau de la tâche parente. Cette approche suit la méthode du guide du langage, qui consiste à accumuler les résultats des tâches enfants dans le corps du groupe de tâches. Consultez Concurrency.

Le collecteur soumet d’abord un nombre limité de tâches enfants. Chaque ligne terminée libère une place pour une autre entrée. Un calcul qui échoue fait donc avancer le lot comme un calcul réussi : tous deux renvoient une ligne ordinaire et la collecte continue.

activeProcessorCount de Foundation indique le nombre de cœurs de traitement actifs disponibles sur l’ordinateur. L’exemple plafonne ce nombre à quatre pour illustrer un choix de planification. Ce plafond ne correspond pas à un optimum mesuré et ne garantit pas que quatre calculs s’exécutent simultanément. Consultez la documentation activeProcessorCount d’Apple.

La limite de concurrence borne le nombre de tâches enfants en attente, tandis que le tableau des résultats continue de grandir avec le nombre d’entrées. Cette approche convient à un petit lot sur ordinateur de bureau. Pour ces calculs de carrés minuscules, l’exemple illustre la gestion des résultats ; il ne prétend pas être plus performant qu’une boucle séquentielle.

La vérification d’une entrée vide renvoie un tableau vide. Une limite de concurrence nulle ou négative est ramenée à un. Les entrées en double restent distinctes, car leur identité dépend de leur position : deux entrées égales produisent donc toujours deux lignes.

Que se passe-t-il lorsque la tâche parente est annulée ?

L’annulation Swift est coopérative. L’annulation d’une tâche parente se propage à ses tâches enfants structurées, mais celles-ci doivent détecter ce signal et y répondre. La documentation TaskGroup d’Apple explique à la fois cette propagation et l’obligation de gérer l’annulation de manière coopérative.

Ici, evaluate appelle Task.checkCancellation() avant le calcul. Apple indique que cette méthode lève CancellationError lorsqu’une annulation est détectée ; l’enveloppe intercepte cette erreur à part et renvoie une ligne d’annulation. Consultez checkCancellation().

Dès que le collecteur détecte l’annulation, il marque comme annulées les entrées qui n’ont pas encore été soumises. Les tâches enfants déjà soumises sont tout de même menées à terme. Leurs résultats réussis restent dans le rapport, et celles qui détectent l’annulation renvoient des lignes d’annulation.

L’annulation peut survenir après la vérification d’une tâche enfant. Celle-ci peut alors tout de même terminer avec succès, ce que ce contrat accepte. Si le calcul devient une longue boucle, ajoutez des vérifications d’annulation à des intervalles utiles à l’intérieur de ce calcul. Comme le précise la documentation de TaskGroup, le groupe attend toujours la fin de toutes les tâches enfants soumises avant de renvoyer un résultat.

Quand faut-il plutôt utiliser un groupe de tâches avec levée d’erreur ?

Si les tâches enfants existantes lèvent déjà des erreurs et que vous souhaitez conserver cette interface, un groupe de tâches avec levée d’erreur permet également de recueillir les résultats individuels. Appelez nextResult() à plusieurs reprises et examinez chaque réussite ou échec renvoyé, jusqu’à ce que le résultat facultatif soit vide. Contrairement à next(), cette méthode renvoie une valeur Result au lieu de transmettre l’erreur de la tâche enfant à l’appelant en la levant. Apple décrit son comportement selon l’ordre d’achèvement dans nextResult().

Pour cette approche, incluez l’identité de l’entrée dans les valeurs réussies et dans toute erreur qui doit être attribuée à une entrée précise. Une erreur arithmétique sans autre précision n’indique pas laquelle de plusieurs entrées l’a produite. Choisissez aussi une politique explicite d’annulation, plutôt que de la traiter comme un échec de calcul ordinaire.

Utilisez l’approche qui renvoie des résultats lorsque l’exigence principale est d’obtenir un rapport complet pour chaque entrée. Utilisez un groupe avec levée d’erreur lorsque le comportement souhaité pour le lot est de propager une erreur hors du groupe, ou lorsque l’utilisation d’opérations existantes avec levée d’erreur via nextResult() est plus pratique.

Pour cette tâche de calcul locale, le contrat essentiel est simple : conserver chaque valeur réussie, consigner chaque entrée qui échoue et poursuivre la collecte. En associant un index d’entrée à chaque résultat, le rapport final reste utile même lorsque les calculs se terminent dans un ordre imprévisible.

Sources