Swift Concurrency : Le guide complet

Publié le · Mis à jour le · 22 min

Wlad
Wlad
Fondateur & Tech Lead Swift

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 :

GCDSwift 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

Ressources officielles