Pour rendre une opération de calcul Swift annulable, conservez son descripteur de tâche, appelez cancel() lorsque l’annulation est demandée et vérifiez si la tâche a été annulée dans la boucle. Cet exemple macOS maintient également l’interface réactive et empêche un résultat tardif de s’afficher après l’annulation d’une exécution.
Que fait réellement l’annulation d’une tâche Swift ?
Swift utilise une annulation coopérative : une demande d’annulation marque une tâche, tandis que le code en cours d’exécution décide quand s’arrêter. Une boucle CPU qui ne vérifie jamais l’annulation peut continuer jusqu’à son terme. L’appel à Task.checkCancellation() génère CancellationError lorsque l’annulation est détectée. Documentation d’Apple sur l’annulation, checkCancellation().
L’objectif concret du lecteur est le suivant : ajouter des boutons Start et Cancel fonctionnels à une petite app Mac qui calcule une somme synthétique. Toutes les entrées sont générées localement. Le calcul additionne les racines carrées d’entiers positifs consécutifs et ne produit un seul Double que lorsque tous les termes demandés ont été traités.
Choisissez le contrat de résultat avant d’implémenter l’annulation. Cet exemple ignore une somme inachevée et signale l’annulation séparément. Renvoyer une somme partielle nécessiterait un autre contrat indiquant combien de termes ont été inclus ; sinon, un appelant pourrait prendre un calcul incomplet pour le résultat demandé. Le guide du langage Swift décrit les erreurs, le renvoi d’un résultat vide et le renvoi d’un calcul partiel comme réponses possibles à l’annulation. Concurrence.
Deux responsabilités sont à mettre en œuvre : arrêter les calculs inutiles et décider si une valeur calculée appartient encore à l’interface. L’exemple gère les deux, y compris le cas où l’annulation est demandée après la dernière vérification du processus de calcul, mais avant l’affichage de son résultat.
Ajouter des vérifications d’annulation dans le calcul
Dans Xcode, créez un projet d’app macOS utilisant SwiftUI et Swift. Pour cet exemple, utilisez le mode de langage Swift 6 et une cible de déploiement macOS 13 ou ultérieure. Ajoutez le fichier complet suivant, nommé SyntheticCalculation.swift. Il ne dépend pas de la vue.
// SyntheticCalculation.swift
import Swift
enum SyntheticCalculation {
nonisolated static func sumOfSquareRoots(
terms: Int,
checkEvery: Int = 4_096
) throws -> Double {
precondition(terms >= 0, "terms must be nonnegative")
precondition(checkEvery > 0, "checkEvery must be positive")
try Task.checkCancellation()
var completed = 0
var total = 0.0
while completed < terms {
try Task.checkCancellation()
let count = min(checkEvery, terms - completed)
let end = completed + count
while completed < end {
completed += 1
total += Double(completed).squareRoot()
}
}
try Task.checkCancellation()
return total
}
}
La fonction vérifie l’annulation avant de commencer, à la limite de chaque bloc et avant de renvoyer le résultat. Ces points d’arrêt sont délibérés : une demande d’annulation pendant un bloc sera détectée à la vérification suivante. Task.checkCancellation() est synchrone ; le calcul peut donc rester une fonction ordinaire qui génère des erreurs. Il n’a pas besoin d’un await simplement pour vérifier l’annulation. checkCancellation().
Le calcul par blocs délimite également clairement les entrées. Pour une tâche qui n’a pas été annulée, zéro terme donne zéro. Les termes négatifs et une taille de bloc non positive sont des erreurs de programmation contrôlées par des préconditions. Le calcul de chaque bloc à partir du nombre restant évite de faire dépasser à sa limite terms.
L’accumulateur reste local au processus de calcul. Une erreur d’annulation interrompt la fonction sans renvoyer cet accumulateur, conformément au contrat choisi, qui exige un résultat complet. Comme il s’agit d’une addition en virgule flottante, la valeur obtenue est une approximation de la somme mathématique.
Maintenir la réactivité du bouton Cancel
Les vérifications d’annulation ne peuvent pas aider si l’action du bouton n’a aucune occasion de s’exécuter. Apple explique que placer une opération synchrone lourde dans un simple Task créé depuis une vue SwiftUI peut tout de même occuper l’acteur principal, car cette tâche hérite du contexte d’acteur environnant. Apple documente les tâches détachées comme un moyen pris en charge d’exécuter du travail synchrone en dehors de ce contexte hérité. Améliorer la réactivité des apps.
Ici, le processus de calcul détaché ne capture qu’un entier immuable et appelle une fonction synchrone explicitement nonisolated. Tout l’état de l’interface reste sur l’acteur principal. Une tâche distincte sur l’acteur principal attend la valeur du processus de calcul et met à jour l’affichage.
Remplacez le fichier d’entrée généré de l’app par le fichier complet ci-dessous et supprimez ContentView.swift s’il a été généré. La cible doit contenir exactement une déclaration @main, conformément au cycle de vie des apps SwiftUI. Ces deux fichiers constituent ensemble l’app d’exemple. App.
// CancellationDemoApp.swift
import Foundation
import SwiftUI
@main
struct CancellationDemoApp: App {
var body: some Scene {
WindowGroup {
CalculationView()
}
}
}
@MainActor
struct CalculationView: View {
@State private var worker: Task<Double, Error>?
@State private var runID: UUID?
@State private var status = "Ready"
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Synthetic square-root sum")
.font(.headline)
Text(status)
.textSelection(.enabled)
HStack {
Button("Start") {
start()
}
.disabled(worker != nil)
Button("Cancel") {
guard let worker else { return }
worker.cancel()
status = "Cancellation requested"
}
.disabled(worker == nil)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 160)
.onDisappear {
worker?.cancel()
runID = nil
worker = nil
status = "Ready"
}
}
private func start() {
guard worker == nil else { return }
let id = UUID()
let terms = 500_000_000
runID = id
status = "Calculating"
let run = Task.detached(priority: .userInitiated) {
try SyntheticCalculation.sumOfSquareRoots(terms: terms)
}
worker = run
Task { @MainActor in
do {
let value = try await run.value
guard runID == id else { return }
if run.isCancelled {
status = "Cancelled"
} else {
status = "Result: \(value)"
}
} catch is CancellationError {
guard runID == id else { return }
status = "Cancelled"
} catch {
guard runID == id else { return }
status = "Failed: \(error)"
}
guard runID == id else { return }
worker = nil
runID = nil
}
}
}
Il est essentiel de conserver worker. Abandonner le descripteur d’une tâche ne l’annule pas ; cela vous empêche de l’annuler à l’aide de ce descripteur. Attendre le value du processus de calcul, qui génère des erreurs, permet également d’observer ses erreurs. Les tâches détachées nécessitent une gestion explicite de leur cycle de vie ; le bouton et le gestionnaire de disparition annulent donc directement le processus de calcul. Annuler uniquement la tâche qui attend son résultat ne garantit pas la propagation de l’annulation à ce processus de calcul détaché. Documentation sur les tâches détachées.
UUID de Foundation fournit un identifiant pour chaque exécution. La comparaison des identifiants est une règle au niveau de l’app : une fois qu’une disparition invalide une exécution, son achèvement ultérieur ne peut pas mettre à jour l’état actuel de cette vue. L’identifiant n’arrête pas le calcul ; worker.cancel() et les vérifications dans la boucle s’en chargent. UUID.
Le bouton Start reste désactivé jusqu’à ce que le processus de calcul se termine ou génère une erreur. Lorsque vous appuyez sur Cancel, le message Cancellation requested s’affiche d’abord ; l’observateur d’achèvement le remplace par Cancelled lorsqu’il traite le résultat. Une demande d’annulation reste ainsi distincte de la confirmation que le processus de calcul s’est arrêté.
Comment gérer un résultat tardif ?
Considérez le scénario suivant : le processus de calcul passe sa dernière vérification, se termine, puis l’utilisateur appuie sur Cancel avant que l’acteur principal n’accepte sa valeur. Apple souligne explicitement qu’une annulation après la dernière vérification possible peut n’avoir aucun effet observable sur le processus de calcul. Une vérification finale réduit cet intervalle, mais ne peut pas le supprimer. cancel().
L’exemple vérifie donc run.isCancelled après avoir attendu une valeur obtenue sans erreur. Il s’agit d’une vérification du descripteur de la tâche de calcul. Utiliser la propriété statique Task.isCancelled à cet endroit examinerait plutôt la tâche observatrice. Le guide du langage Swift distingue la vérification de la tâche actuelle au moyen de la propriété de type et celle d’une autre tâche au moyen de sa propriété d’instance. Concurrence.
Cela établit une règle d’acceptation concrète : si l’annulation est traitée avant l’acceptation du résultat, elle l’emporte. Une fois l’acceptation terminée et le processus de calcul effacé, le bouton Cancel désactivé n’a plus de résultat achevé à retirer. Cette règle suit l’ordre des actions sur l’acteur principal au lieu de promettre que l’annulation peut annuler un travail terminé.
Choisir un intervalle de vérification d’annulation utile
La taille de bloc par défaut de 4,096 termes est un point de départ illustratif. Des blocs plus petits offrent plus souvent l’occasion de détecter l’annulation et entraînent davantage de vérifications. Des blocs plus grands réduisent la fréquence des vérifications, mais laissent davantage de calculs entre les points d’arrêt.
À titre de calcul approximatif, supposons qu’un bloc contienne 4,096 termes et que chaque terme prenne deux microsecondes, à titre d’exemple. Ce bloc représente environ 8,2 millisecondes de calcul. Ces valeurs sont inventées pour illustrer le calcul ; elles ne constituent pas des mesures de Swift ni d’un Mac particulier. La planification et les mises à jour de l’interface peuvent ajouter du délai.
| Observation | Que vérifier ou modifier |
|---|---|
| L’interface cesse de réagir pendant le calcul | Vérifiez que la boucle s’exécute dans le processus de calcul détaché, hors de l’isolation de l’acteur principal. |
| L’interface réagit, mais l’annulation prend trop de temps | Réduisez checkEvery et mesurez la durée maximale d’un bloc. |
| Une opération coûteuse est ajoutée à un bloc | Ajoutez des points d’arrêt dans cette opération si son implémentation le permet. |
| Une exécution annulée affiche un résultat | Vérifiez le descripteur du processus de calcul au moment de l’acceptation et conservez la protection par identifiant d’exécution. |
N’ajoutez pas de gestionnaire d’annulation simplement pour définir un autre indicateur destiné à cette boucle. La tâche expose déjà son état d’annulation et le guide du langage Swift explique que l’installation d’un gestionnaire laisse l’arrêt coopératif. Pour ce calcul local, des vérifications explicites fournissent le mécanisme d’arrêt nécessaire. Concurrence.
Vérifier l’achèvement, l’annulation et les exécutions successives
Le compilateur Swift et l’environnement d’exécution macOS n’étaient pas disponibles dans l’environnement de recherche ; cet exemple a donc été examiné, mais ni compilé ni exécuté. Compilez les deux fichiers dans Xcode, puis vérifiez les comportements suivants séparément :
- Petit résultat obtenu : remplacez temporairement
termspar4. Le résultat affiché devrait être proche de6.146264369941973, à l’arrondi en virgule flottante près. - Entrée vide : remplacez
termspar0. Une exécution non annulée devrait afficher zéro. - Annulation pendant le calcul : rétablissez
500_000_000, appuyez surStart, puis surCancelpendant l’exécution. Le messageCancellation requesteddevrait s’afficher, suivi deCancelled, sans résultat partiel. - Nouvelle exécution : après la fin de l’annulation, appuyez de nouveau sur
Start. La nouvelle exécution devrait commencer normalement. - Disparition de la vue : fermez une fenêtre pendant un calcul. Le gestionnaire de disparition devrait demander l’annulation et invalider les mises à jour de l’interface de cette exécution.
Le grand nombre de termes vise à laisser le temps d’interagir, mais la durée dépend du Mac et de la configuration de compilation. Si le calcul se termine avant que vous puissiez l’annuler, augmentez le nombre dans la limite de la plage d’entiers prise en charge. Vérifiez l’arithmétique de fin avec la petite entrée au lieu d’attendre la fin de la longue démonstration.
La conception obtenue définit clairement l’arrêt et l’acceptation : le bouton annule le processus de calcul conservé, les limites des blocs permettent au calcul de générer une erreur et l’acteur principal n’accepte qu’un résultat non annulé provenant de l’exécution actuelle. Ces trois choix rendent l’annulation utile pour un calcul Mac ordinaire.
Sources
- Concurrence — Le langage de programmation Swift, projet Swift.
- checkCancellation() — Documentation Apple pour les développeurs.
- cancel() — Documentation Apple pour les développeurs.
- Améliorer la réactivité des apps — Documentation Apple pour les développeurs.
- detached(name:priority:operation:) — Documentation Apple pour les développeurs.
- App — Documentation Apple pour les développeurs.
- UUID — Documentation Apple pour les développeurs.