Swift Concurrency : Le guide complet
Publié le · Mis à jour le · 22 min
Swift Concurrency a révolutionné la programmation asynchrone sur les plateformes Apple. Introduit avec Swift 5.5 en 2021, le modèle a considérablement évolué jusqu'à Swift 6.2 qui simplifie enfin son utilisation avec "Approachable Concurrency". Ce guide couvre tout ce que vous devez savoir, du novice à l'expert.
Pourquoi Swift Concurrency ?
Avant Swift 5.5, la programmation asynchrone reposait sur des callbacks, des closures de completion, et Grand Central Dispatch (GCD). Ce modèle fonctionnait mais créait des problèmes :
Pyramid of doom avec les callbacks imbriqués
Gestion d'erreurs fragmentée et facile à oublier
Aucune protection contre les data races
Code difficile à lire et à maintenir
Swift Concurrency résout ces problèmes avec un modèle intégré au langage. Le compilateur vérifie la sécurité des accès concurrents et le code async ressemble à du code synchrone.
Fondamentaux : async/await
Le mot-clé async marque une fonction qui peut suspendre son exécution. Le mot-clé await indique un point de suspension potentiel.
Un point crucial : await ne bloque jamais le thread. Il libère le thread pour d'autres tâches pendant l'attente. C'est fondamentalement différent d'un sleep ou d'une attente synchrone.
Suspension et continuations
Quand le runtime rencontre un await, il découpe votre fonction en "partial tasks". Chaque segment entre deux await est une unité d'exécution que le runtime peut planifier indépendamment.
Le runtime peut intercaler ces partial tasks avec celles d'autres fonctions async, optimisant l'utilisation des ressources système.
Task : l'unité de travail asynchrone
Task est le conteneur fondamental du travail asynchrone. Il fournit un contexte d'exécution, gère les priorités, et propage l'annulation.
Types de Task
Swift propose plusieurs types de Task selon vos besoins :
La différence entre Task et Task.detached est importante. Une Task standard hérite du contexte de l'appelant (acteur, priorité, task-local values). Une Task.detached démarre dans un contexte vierge.
Priorités des Tasks
Swift définit plusieurs niveaux de priorité qui influencent l'ordonnancement :
Le runtime peut aussi faire de la "priority escalation" : si une tâche haute priorité attend une tâche basse priorité, cette dernière est temporairement promue.
async let : parallélisme simple
Quand vous avez plusieurs opérations indépendantes, async let les exécute en parallèle :
Attention à la différence avec l'exécution séquentielle :
async let crée des child tasks qui sont automatiquement annulées si une erreur survient ou si la portée se termine.
Annulation coopérative
Swift utilise un modèle d'annulation coopérative. Quand vous annulez une Task, elle n'est pas tuée brutalement. Le runtime la marque comme annulée, et c'est à votre code de vérifier et de réagir.
Deux méthodes pour vérifier l'annulation :
withTaskCancellationHandler
Pour les opérations longues avec des callbacks, utilisez withTaskCancellationHandler :
Gestion des erreurs async
Les fonctions async throws propagent les erreurs naturellement :
Avec async let, si une tâche parallèle échoue, les autres sont automatiquement annulées :
Intégration SwiftUI
SwiftUI s'intègre naturellement avec Swift Concurrency via le modifier .task :
Le modifier .task(id:) relance automatiquement quand l'identifiant change :
Swift 6.2 : Approachable Concurrency
Swift 6.2 (Xcode 26) introduit des changements majeurs pour simplifier la concurrence. Le document "Improving the approachability of data-race safety" reconnaît que le modèle était devenu trop complexe.
MainActor par défaut (SE-0466)
Le changement le plus impactant : vous pouvez configurer un module entier pour s'exécuter sur le MainActor par défaut.
Avec ce réglage, tout le code sans annotation explicite s'exécute sur le MainActor :
Dans Xcode 26, les nouveaux projets ont cette option activée par défaut.
Async sur l'acteur appelant (SE-0461)
Avant Swift 6.2, les fonctions async nonisolated sautaient automatiquement sur un thread background :
Avec Swift 6.2, les fonctions async restent sur l'acteur de l'appelant :
Ce comportement est activé via l'upcoming feature NonisolatedNonsendingByDefault.
@concurrent : opt-in explicite
Si vous voulez qu'une fonction s'exécute vraiment en parallèle, Swift 6.2 introduit @concurrent :
Cette distinction explicite rend le code beaucoup plus lisible.
Conformances isolées (SE-0470)
SE-0470 permet de restreindre une conformance de protocole à un acteur spécifique :
Sans @MainActor Equatable, le compilateur pourrait exécuter == sur n'importe quel thread, violant l'isolation de la classe.
Bonnes pratiques
1. Préférer la concurrence structurée
Utilisez async let et TaskGroup plutôt que Task.detached quand possible. La concurrence structurée garantit que les child tasks sont nettoyées automatiquement.
2. Vérifier l'annulation régulièrement
Dans les boucles longues, vérifiez périodiquement l'annulation :
3. Éviter les captures fortes dans les Tasks
Utilisez [weak self] pour éviter les cycles de rétention :
4. Utiliser le bon niveau d'isolation
Ne mettez pas tout sur le MainActor. Réservez-le pour le code UI :
Pièges courants
1. Oublier await
Le compilateur vous avertit, mais attention aux contextes où await est implicite :
2. Bloquer le MainActor
Évitez les opérations longues sur le MainActor :
3. Task.sleep vs synchronous sleep
Utilisez toujours Task.sleep pour les délais :
Migration depuis GCD
Si vous avez du code GCD existant, voici les équivalences :
| GCD | Swift Concurrency |
|---|---|
DispatchQueue.main.async | Task { @MainActor in } |
DispatchQueue.global().async | Task.detached { } |
DispatchGroup | TaskGroup |
DispatchSemaphore | AsyncStream ou Actors |
DispatchWorkItem.cancel() | task.cancel() |
Exemple de migration :
Pour aller plus loin
Swift Concurrency est un sujet vaste. Voici les prochaines étapes recommandées :
TaskGroup et ThrowingTaskGroup pour le parallélisme dynamique
Actors pour l'isolation de données partagées
AsyncStream pour les flux de valeurs asynchrones
Sendable et l'isolation des données