Utilisez un groupe de tâches Swift pour recueillir les résultats des calculs concurrents dans leur ordre d’achèvement, ignorer nil et annuler les tâches enfants restantes dès qu’une valeur est obtenue. Cet exemple en ligne de commande pour Mac trouve une disposition rectangulaire exploitable pour des tuiles synthétiques et explique dans quels cas le résultat peut effectivement être renvoyé.
Définir ce qui constitue le premier résultat utile
Supposons qu’un petit outil de mise en page pour ordinateur doive disposer 221 tuiles synthétiques en rectangle. Plusieurs calculs locaux recherchent différents nombres possibles de rangées. Tout facteur non trivial convient : 13 rangées et 17 colonnes fonctionnent, tout comme 17 rangées et 13 colonnes. La tâche du lecteur consiste à accepter le premier calcul valide qui se termine.
Il s’agit d’un contrat fondé sur l’ordre d’achèvement. Il ne sélectionne ni le plus petit facteur, ni le rectangle le plus équilibré, ni le premier calcul de la liste. Apple indique que les résultats successifs d’un groupe de tâches arrivent dans leur ordre d’achèvement, indépendamment de l’ordre d’insertion. TaskGroup.next(isolation:)
Définissez le comportement avant d’écrire l’utilitaire :
| Situation | Comportement requis |
|---|---|
Un calcul se termine avec nil |
Continuer à attendre les autres calculs. |
| Un calcul se termine avec une valeur | La sélectionner et demander l’annulation du travail restant. |
Tous les calculs renvoient nil |
Renvoyer nil. |
| La liste des calculs est vide | Renvoyer nil. |
| L’annulation du parent est observée | Cesser de sélectionner des résultats et renvoyer nil. |
La dernière ligne correspond à un choix délibéré de l’API pour cet exemple. Ici, nil signifie que l’invocation ne fournit aucune valeur exploitable ; cela ne permet pas de distinguer une recherche infructueuse d’une annulation. Si votre appelant a besoin de cette distinction, utilisez un type de résultat explicite ou un contrat avec exceptions.
Créer un exemple local complet sur votre Mac
Utilisez une chaîne d’outils Swift 6 ou ultérieure. Dans Terminal, vérifiez-la avec swift --version. Si Swift n’est pas disponible, suivez les instructions officielles Installer Swift — macOS. Le programme utilise Foundation et les API de concurrence de Swift sans dépendances de paquets externes.
Créez un nouveau paquet exécutable avec ces commandes. Cette procédure suit le flux documenté par Swift.org pour créer et exécuter un paquet. Créer un outil en ligne de commande
mkdir FirstNonNilDemo
cd FirstNonNilDemo
swift package init --name FirstNonNilDemo --type executable
Remplacez le fichier source exécutable généré par le programme complet ci-dessous. Selon la disposition générée, il peut s’appeler Sources/FirstNonNilDemo/FirstNonNilDemo.swift ou Sources/main.swift. Utilisez un seul fichier d’entrée nommé FirstNonNilDemo.swift ; si le modèle a créé main.swift, renommez ce fichier avant d’ajouter le programme @main. Le guide de Swift indique qu’un programme utilise soit main.swift, soit un point d’entrée @main. Créer un outil en ligne de commande
import Foundation
func firstNonNil<Value: Sendable>(
_ calculations: [@Sendable () async -> Value?]
) async -> Value? {
guard !Task.isCancelled else { return nil }
return await withTaskGroup(
of: Value?.self,
returning: Value?.self
) { group in
defer { group.cancelAll() }
for calculation in calculations {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await calculation()
}
guard added else { break }
}
for await candidate in group {
guard !Task.isCancelled else { return nil }
if let value = candidate {
return value
}
}
return nil
}
}
func findFactor(total: Int, range: ClosedRange<Int>) -> Int? {
guard total > 1 else { return nil }
for rows in range {
guard !Task.isCancelled else { return nil }
guard rows > 1, rows < total else { continue }
if total % rows == 0 {
return rows
}
}
return nil
}
@main
struct FirstNonNilDemo {
static func main() async {
let checks: [@Sendable () async -> Int?] = [
{ nil },
{ 42 }
]
let checkResult = await firstNonNil(checks)
precondition(checkResult == 42)
let empty: [@Sendable () async -> Int?] = []
let emptyResult = await firstNonNil(empty)
precondition(emptyResult == nil)
for rawText in [" 221\n", " 223 ", "tiles"] {
let text = rawText.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard let total = Int(text), total > 1 else {
print("Input must be a whole number greater than 1.")
continue
}
let calculations: [@Sendable () async -> Int?] = [
{ findFactor(total: total, range: 2...5) },
{ findFactor(total: total, range: 6...16) },
{ findFactor(total: total, range: 17...30) }
]
if let rows = await firstNonNil(calculations) {
print("\(total) tiles: \(rows) rows x \(total / rows) columns")
} else {
print("\(total) tiles: no factor in the supplied ranges.")
}
}
}
}
Exécutez le paquet depuis son répertoire :
swift run FirstNonNilDemo
Pour 221, 221 tiles: 13 rows x 17 columns ou 221 tiles: 17 rows x 13 columns conviennent. Pour 223, le programme indique qu’il n’y a aucun facteur dans les plages fournies. Le texte tiles affiche le message relatif à la saisie. Il s’agit des résultats attendus, et non d’une transcription d’exécution. Les calculs arithmétiques ont été vérifiés séparément ; aucun compilateur Swift n’était disponible dans l’environnement de recherche, donc le programme Swift a été examiné, mais pas exécuté.
Foundation ne gère que le nettoyage des espaces blancs environnants. Sa méthode trimmingCharacters(in:) supprime les caractères correspondants aux deux extrémités ; elle ne supprime pas les caractères à l’intérieur de la chaîne. Ainsi, 221 devient 221, tandis que 2 21 reste invalide pour ce programme. trimmingCharacters(in:)
Pourquoi l’utilitaire ignore nil sans perdre les résultats suivants
Le type des résultats des tâches enfants du groupe est Value?. Chaque tâche enfant fournit donc soit une valeur exploitable, soit un résultat vide ordinaire. La boucle for await reçoit les résultats des tâches enfants à mesure qu’elles se terminent, et sa liaison conditionnelle interne ne sélectionne qu’un candidat non nil.
Si vous utilisez directement group.next(), il existe deux types de valeurs vides distincts. Son type de retour documenté est ChildTaskResult?. Avec des résultats de tâches enfants optionnels, cela devient Value?? : l’optionnel externe indique si un autre résultat de tâche enfant existe, tandis que l’optionnel interne indique si cette tâche enfant a trouvé une valeur. TaskGroup.next(isolation:)
Cette distinction explique une erreur fréquente : récupérer un seul résultat et le renvoyer immédiatement. Si le premier calcul terminé renvoie nil, d’autres tâches enfants peuvent encore avoir des réponses utiles. La boucle de l’exemple complet continue à consommer les résultats jusqu’à trouver une valeur ou jusqu’à épuisement du groupe.
L’utilitaire utilise addTaskUnlessCancelled et vérifie sa valeur booléenne de retour. Apple indique que cette méthode refuse de créer une tâche enfant si le groupe est déjà annulé. Interrompre la boucle de soumission évite de continuer à soumettre du travail après la détection de cette condition. addTaskUnlessCancelled(priority:operation:)
Les calculs capturent le nombre immuable de tuiles et renvoient leurs réponses par l’intermédiaire du groupe. Ils ne mettent pas à jour une variable partagée qui contiendrait le résultat gagnant. Le résultat générique doit respecter la contrainte Sendable, conformément à la contrainte documentée pour les groupes de tâches. withTaskGroup(of:returning:isolation:body:)
Pourquoi sélectionner un gagnant ne signifie pas le renvoyer immédiatement
Renvoyer une valeur depuis la fermeture du groupe sélectionne la réponse, mais le withTaskGroup englobant attend toujours que toutes les tâches enfants se terminent. Apple indique explicitement que même les tâches enfants annulées doivent se terminer avant que la fonction ne renvoie un résultat. withTaskGroup(of:returning:isolation:body:)
Le bloc defer appelle group.cancelAll() à chaque sortie de la fermeture. Cela demande l’annulation des calculs restants. L’annulation n’interrompt pas les calculs arbitraires : les tâches enfants doivent coopérer et se terminer plus tôt d’elles-mêmes. TaskGroup
Dans cet exemple, findFactor vérifie Task.isCancelled avant chaque diviseur candidat. Chaque unité de travail est petite, ce qui offre de fréquentes occasions de s’arrêter. Pour un calcul plus important, placez les vérifications entre des blocs de taille limitée et significatifs. Une vérification effectuée uniquement avant une longue opération indivisible laisse cette opération se poursuivre jusqu’à son terme. Le guide du langage Swift décrit l’annulation comme coopérative et indique que renvoyer nil est l’une des réponses possibles. Concurrence
Si un calcul concurrent ne se termine jamais et ne réagit jamais à l’annulation, cet utilitaire structuré ne peut pas garantir un retour rapide. La solution consiste à faire en sorte que chaque tâche de travail soit bornée ou sensible à l’annulation. Ajouter un résultat concurrent pour le délai d’expiration ne supprime pas l’obligation du groupe d’attendre ses tâches enfants.
L’annulation du parent est vérifiée avant la soumission, à l’entrée de la tâche enfant, dans la boucle arithmétique et avant l’acceptation de chaque résultat. Ces vérifications observent l’annulation à des moments précis ; elles ne créent pas de règle atomique selon laquelle l’annulation l’emporte toujours sur une valeur. Une annulation survenant après la dernière vérification peut coïncider avec un retour réussi.
Choisir la bonne règle de sélection et vérifier ses limites
L’exemple synthétique fournit un critère de décision utile : 13 et 17 satisfont tous deux le contrat énoncé. Si votre outil exige le plus petit nombre de rangées, accepter l’un ou l’autre résultat est incorrect. L’ordre d’achèvement n’est utile que si toutes les réponses acceptables sont interchangeables pour la tâche du lecteur.
| Besoin de l’appelant | Approche adaptée |
|---|---|
| N’importe quelle valeur acceptable à mesure que les calculs se terminent | Utiliser la boucle de groupe de tâches présentée ci-dessus. |
| Le premier calcul réussi dans une liste de préférences fixe | Évaluer dans l’ordre des préférences, ou conserver les index et résoudre les candidats précédents avant de choisir. |
| Le résultat le plus petit ou celui qui obtient le meilleur score | Comparer les résultats pertinents avant de sélectionner. |
| Plusieurs résultats connus nécessaires ensemble | Envisager async let. |
Le guide du langage Swift présente async let pour des tâches enfants connues dont les résultats seront nécessaires plus tard, et les groupes de tâches pour un contrôle explicite des tâches enfants ajoutées dynamiquement. Attendre des résultats nommés dans un ordre choisi ne met pas en œuvre une boucle de sélection fondée sur l’ordre d’achèvement. Concurrence
Utilisez ces vérifications pour adapter le programme :
- Conservez l’assertion sur la liste vide : elle doit renvoyer
nil. - Conservez l’assertion mêlant
nilet42: elle doit renvoyer42, quel que soit l’ordre d’achèvement. - Testez des calculs qui renvoient tous
nil. - Pour plusieurs réponses valides, vérifiez l’appartenance à l’ensemble des réponses valides plutôt qu’un gagnant précis.
- Examinez chaque tâche de travail pour vérifier que sa progression est bornée et qu’elle comporte des vérifications d’annulation.
- Indiquez qu’une recherche infructueuse se limite aux plages fournies ; n’en déduisez pas qu’aucune réponse n’existe ailleurs.
Les trois petites recherches de facteurs illustrent le comportement, pas un gain de vitesse. Une recherche séquentielle suffit pour ces nombres donnés à titre d’exemple. Utilisez le modèle de calculs concurrents lorsque la tâche réelle gagne à accepter des résultats interchangeables, et mesurez cette charge de travail avant de décider du niveau de concurrence.
Explicitez la règle d’acceptation, ignorez les résultats vides des tâches enfants et faites en sorte que le travail restant puisse s’arrêter. L’outil Mac dispose ainsi d’un contrat de valeur prévisible : le premier résultat exploitable transmis par le groupe, suivi de l’arrêt coopératif des tâches enfants.
Sources
- Installer Swift — macOS — Swift.org
- Créer un outil en ligne de commande — Swift.org
- TaskGroup — Apple
- withTaskGroup(of:returning:isolation:body:) — Apple
- next(isolation:) — Apple
- addTaskUnlessCancelled(priority:operation:) — Apple
- trimmingCharacters(in:) — Apple
- Concurrence — The Swift Programming Language, Swift.org