Pour une app Mac qui doit terminer des calculs asynchrones locaux dans l’ordre de soumission, utilisez une méthode de soumission synchrone isolée à l’acteur principal qui chaîne chaque tâche à la précédente. L’isolation par un acteur ne garantit pas à elle seule cet ordre.
Ce guide construit une petite file d’attente pour des calculs d’entiers synthétiques. Il définit précisément le moment de la soumission, préserve l’ordre aux points de suspension, renvoie le résultat de chaque calcul et permet aux tâches suivantes de continuer lorsqu’une tâche échoue.
Que signifie l’ordre strict de soumission ?
Supposons qu’une calculatrice de bureau soumette les tâches A, B et C. Le contrat requis est le suivant : le calcul de A se termine avant que celui de B commence, et celui de B se termine avant que celui de C commence. Attendre trois tâches lancées indépendamment dans l’ordre alphabétique ne ferait qu’ordonner l’observation de leurs résultats.
La frontière de soumission est importante. Dans cette implémentation, la soumission a lieu lorsque la méthode submit, isolée à l’acteur principal, s’exécute et installe le maillon suivant de la chaîne. Des appels synchrones consécutifs depuis un même contexte de l’acteur principal établissent l’ordre A, B, C sans suspension entre eux.
Si des tâches indépendantes demandent à soumettre simultanément, leur ordre d’arrivée n’est pas spécifié. La file d’attente préserve l’ordre dans lequel sa méthode de soumission les accepte. Si votre application exige un ordre particulier entre ces producteurs, établissez cet ordre avant la soumission, par exemple en regroupant un lot fini dans un tableau explicitement ordonné.
La conception utilise l’acteur principal pour la brève étape de gestion. Apple définit MainActor comme un acteur singleton dont l’exécuteur est équivalent à la file d’envoi principale. Le calcul proprement dit ci-dessous est confié à un acteur de travail distinct. Documentation de MainActor
Pourquoi un acteur ou une file d’opérations série ne suffisent-ils pas ?
Un acteur exécute le travail isolé à cet acteur une unité à la fois, mais une méthode asynchrone peut se suspendre et permettre à un autre appel de continuer. La conception des acteurs de Swift indique aussi explicitement que les tâches en attente d’un acteur ne sont pas garanties de s’exécuter dans l’ordre où elles l’ont initialement attendu. Par conséquent, placer un calcul asynchrone entier dans un acteur n’établit pas à lui seul le comportement « terminer avant le démarrage suivant ». SE-0306 : Acteurs
Créer des instances Task distinctes ne crée aucune dépendance entre elles. Les tâches peuvent commencer à s’exécuter après leur création ; conserver leurs handles permet de les attendre ou de les annuler, mais n’établit pas de séquence d’exécution. Documentation de Task
Foundation propose un autre mécanisme d’ordonnancement. OperationQueue planifie les opérations selon leur état de préparation, leur priorité et leurs dépendances ; Apple recommande d’utiliser des dépendances lorsqu’un ordre d’exécution précis est important. Définir maxConcurrentOperationCount sur 1 limite la concurrence, mais l’exigence d’ordre nécessite toujours des dépendances explicites. Documentation d’OperationQueue
Choisissez le mécanisme en fonction des besoins déjà présents dans votre application :
| Situation | Approche adaptée | Qu’est-ce qui établit l’ordre ? |
|---|---|---|
| Un lot connu dans une fonction asynchrone | Une boucle qui attend chaque calcul avant de continuer | Le flux de contrôle séquentiel |
| Des tâches arrivent au fil du temps à la suite d’actions sur le bureau | Une méthode de soumission synchrone avec une chaîne de tâches | Chaque calcul attend le signal de fin de son prédécesseur |
| Des opérations Foundation déjà en place | Des dépendances configurées avant la mise en file d’attente | Chaque opération dépend de l’état de fin de l’opération précédente |
La chaîne de tâches est utile ici, car la soumission renvoie immédiatement un handle tandis que les calculs s’exécutent ensuite. Pour un lot fixe sans nouvelles tâches entrantes, la boucle séquentielle demande généralement moins de code.
Créer une file d’attente qui attend la fin des calculs asynchrones
La file d’attente stocke une tâche de fin privée et non susceptible d’échouer, appelée tail. Chaque soumission capture la tâche de fin actuelle, crée une tâche de calcul qui l’attend, puis remplace la tâche de fin par une nouvelle tâche qui observe la fin du calcul.
La tâche de fin attend result, qui représente une réussite ou un échec une fois une tâche terminée. Ainsi, un calcul en échec peut libérer la tâche suivante sans que l’erreur disparaisse du handle du calcul lui-même. Les appelants peuvent toujours récupérer cette erreur via le handle. Documentation sur le résultat de Task
Voici une démonstration complète en ligne de commande pour un Mac doté de la chaîne d’outils Swift 6. Enregistrez-la sous le nom OrderedDemo.swift. Le code source a été examiné, mais n’a pas pu être compilé dans cet environnement de recherche, car aucun compilateur Swift n’y était disponible.
import Foundation
@MainActor
final class OrderedIntQueue {
private var tail: Task<Void, Never>?
func submit(
_ operation: @escaping @Sendable () async throws -> Int
) -> Task<Int, Error> {
let predecessor = tail
let job = Task<Int, Error> {
if let predecessor {
await predecessor.value
}
try Task.checkCancellation()
return try await operation()
}
tail = Task<Void, Never> {
_ = await job.result
}
return job
}
}
enum CalculationError: Error {
case inputOutOfRange
}
actor CalculationWorker {
private var events: [String] = []
func square(_ input: Int, label: String) async throws -> Int {
events.append("start \(label)")
guard (-1000...1000).contains(input) else {
throw CalculationError.inputOutOfRange
}
await Task.yield()
try Task.checkCancellation()
let answer = input * input
events.append("finish \(label)")
return answer
}
func trace() -> [String] {
events
}
}
@main
struct OrderedDemo {
@MainActor
static func main() async {
let queue = OrderedIntQueue()
let worker = CalculationWorker()
let a = queue.submit {
try await worker.square(3, label: "A")
}
let b = queue.submit {
try await worker.square(4, label: "B")
}
let c = queue.submit {
try await worker.square(5, label: "C")
}
var answers: [Int] = []
for job in [a, b, c] {
switch await job.result {
case .success(let answer):
answers.append(answer)
case .failure(let error):
print("Calculation failed: \(error)")
}
}
let events = await worker.trace()
precondition(answers == [9, 16, 25])
precondition(events == [
"start A", "finish A",
"start B", "finish B",
"start C", "finish C"
])
print(events.joined(separator: "\n"))
print("Answers: \(answers)")
}
}
Compilez et exécutez le programme enregistré depuis son répertoire dans Terminal :
swiftc -swift-version 6 -parse-as-library OrderedDemo.swift -o OrderedDemo
./OrderedDemo
La sortie attendue découle des entrées de l’exemple et de sa chaîne de dépendances :
start A
finish A
start B
finish B
start C
finish C
Answers: [9, 16, 25]
Les assertions vérifient à la fois les valeurs renvoyées et les limites des calculs. Le simple fait d’afficher les résultats en attendant les handles dans l’ordre de soumission ne garantirait pas que les calculs se sont exécutés dans cet ordre.
L’acteur de travail appelle volontairement Task.yield() afin que le calcul ait l’occasion de se suspendre. La cession permet à d’autres tâches de s’exécuter, même si l’exécuteur peut reprendre immédiatement la même tâche. L’ordre de la file d’attente découle donc de ses dépendances, et non de l’hypothèse que la cession changera de tâche. Documentation sur la cession d’une tâche
Pourquoi la chaîne préserve-t-elle l’ordre aux points de suspension ?
Trois détails établissent cette garantie. Premièrement, submit ne contient aucun point de suspension. Une autre soumission sur l’acteur principal ne peut donc pas s’intercaler pendant qu’elle capture et remplace tail. Swift indique que le code isolé à un acteur et dépourvu de points de suspension s’exécute sans qu’une autre tâche de cet acteur interrompe cette séquence. Concurrence : acteurs
Deuxièmement, chaque tâche attend son prédécesseur capturé avant d’appeler la fermeture de calcul. L’accès à value d’une tâche non susceptible d’échouer attend la fin de cette tâche. Le traitement d’attente de la tâche suivante peut s’exécuter plus tôt, mais son calcul ne peut pas commencer avant. Documentation sur value d’une tâche non susceptible d’échouer
Troisièmement, le signal de fin du prédécesseur ne s’achève qu’une fois que la tâche précédente a produit son résultat ou son erreur. En remontant les maillons, on établit la séquence pour chaque soumission acceptée : C attend le signal de fin de B, qui attend B, qui attend le signal de fin de A.
Cette garantie d’ordre porte sur le travail attendu à l’intérieur de chaque fermeture soumise. Si une fermeture lance une autre tâche non structurée puis se termine sans l’attendre, ce travail supplémentaire peut se poursuivre après la fermeture. Incluez toutes les étapes de calcul nécessaires dans l’opération attendue avant de la terminer.
Pour l’intégration au bureau, conservez une instance de la file d’attente dans le contrôleur ou le modèle isolé à l’acteur principal qui gère les actions concernées. Appelez directement submit depuis ce contexte, puis utilisez le handle renvoyé pour observer la fin. Créer d’abord une tâche indépendante et ne la soumettre qu’après son démarrage déplacerait la frontière de soumission au moment où cette tâche s’exécute finalement.
Quel comportement adopter pour les erreurs et l’annulation ?
Cette implémentation applique une politique de poursuite après échec. Une tâche en échec conserve son erreur, son signal de fin s’achève et la tâche acceptée suivante devient admissible au calcul. La démonstration examine le result de chaque handle au lieu d’abandonner les résultats restants après la première erreur.
Pour vérifier simplement le traitement d’un échec, remplacez l’entrée de B, 4, par 1001. L’acteur de travail la rejette avant la multiplication. Mettez à jour les réponses attendues avec [9, 25] et la trace attendue avec start A, finish A, start B, start C, finish C. Le programme devrait afficher l’échec et calculer quand même C. La limite d’entrée est un contrat d’illustration choisi pour garder ces opérations arithmétiques simples.
L’annulation est coopérative. La file d’attente vérifie l’annulation après avoir attendu son prédécesseur, et l’acteur de travail la vérifie de nouveau après la cession. Task.checkCancellation() lance CancellationError lorsqu’une annulation a été demandée. Documentation sur checkCancellation de Task
Pour vérifier l’annulation avant l’exécution, appelez b.cancel() immédiatement après la soumission de C et avant la boucle des résultats. Avec ces soumissions ininterrompues sur l’acteur principal, B ne peut pas encore avoir commencé son calcul. Modifiez les réponses attendues en [9, 25] et retirez les deux entrées de B de la trace attendue. B attend toujours le signal de fin de A avant de signaler son annulation ; C poursuit ensuite.
Gardez les signaux de fin privés. Ils servent à la gestion de la chaîne, tandis que les handles renvoyés représentent les calculs individuels. Annuler une tâche ne signifie pas qu’un calcul précédent doit s’arrêter, ni que les calculs suivants doivent commencer pendant qu’il est encore en cours.
Si votre application doit plutôt arrêter tout le lot après un échec, une politique explicite de traitement du lot est nécessaire. Modifier le signal pour propager les erreurs changerait les tâches suivantes qui s’exécutent et la manière de rapporter leurs résultats.
Quand utiliser les dépendances de Foundation à la place ?
Si l’application de bureau représente déjà le travail sous forme d’objets Operation de Foundation, établissez une dépendance de chaque opération envers son prédécesseur avant de la mettre en file d’attente. Foundation considère la dépendance satisfaite lorsque le prédécesseur se termine, qu’il ait réussi ou échoué ; tout comportement dépendant de la réussite nécessite un traitement distinct du résultat. Documentation d’Operation
Soyez prudent lorsque vous adaptez des fonctions asynchrones Swift à ce modèle. Une opération synchrone qui démarre une Task puis se termine immédiatement ne représente pas toute la durée de vie de cette tâche. Une opération asynchrone personnalisée doit gérer correctement son exécution et son état de fin, y compris les notifications requises, afin que les opérations dépendantes attendent le travail voulu. Apple décrit ces responsabilités dans la documentation sur les opérations asynchrones et la déclaration de l’état de fin. Documentation sur le cycle de vie d’Operation
Pour la calculatrice asynchrone locale développée ici, la chaîne de tâches rend le contrat visible : accepter les soumissions de façon synchrone, attendre la fin du prédécesseur, exécuter le calcul complet et exposer son résultat. Préservez ces limites lorsque vous reliez la file d’attente aux actions sur le bureau, puis validez l’exécution à l’aide de traces de début et de fin ainsi que des valeurs renvoyées.
Sources
- MainActor — Apple
- SE-0306 : Acteurs — Swift
- Task — Apple
- OperationQueue — Apple
- result — Apple
- yield() — Apple
- Concurrence — Swift
- value : tâche non susceptible d’échouer — Apple
- checkCancellation() — Apple
- Operation — Apple