Concurrence et multithreading en Swift : niveau avancé
La concurrence (concurrency) est l'un des sujets les plus complexes et importants dans le développement iOS/macOS. Au niveau avancé, Swift propose des outils puissants : du classique Grand Central Dispatch (GCD) au modèle moderne async/await et au système strict des Actors. Dans cet article, nous analyserons comment gérer efficacement les threads, éviter les conditions de course et écrire du code thread-safe.
1. Grand Central Dispatch (GCD) : travailler avec des files d'attente et des groupes
GCD reste la base du multithreading en Swift. Cependant, au niveau avancé, il est important de comprendre non seulement la différence entre main et global(), mais aussi de savoir gérer les dépendances entre tâches.
Création de files d'attente personnalisées
let serialQueue = DispatchQueue(label: "com.pythonlib.serial")let concurrentQueue = DispatchQueue(label: "com.pythonlib.concurrent", attributes: .concurrent)
serialQueue.async { print("Tâche 1 dans une file d'attente séquentielle")}concurrentQueue.async { print("Tâche 2 dans une file d'attente parallèle")}DispatchGroup : synchronisation de plusieurs tâches
let group = DispatchGroup()let queue = DispatchQueue.global()
queue.async(group: group) { sleep(1) print("Chargement des données 1")}queue.async(group: group) { sleep(2) print("Chargement des données 2")}
group.notify(queue: DispatchQueue.main) { print("Tous les chargements terminés, mise à jour de l'UI")}Utilisez DispatchGroup lorsque vous devez attendre la fin de plusieurs opérations parallèles (par exemple, chargement d'images ou analyse JSON).
2. Modèle moderne async/await et Task
Depuis Swift 5.5, le langage a obtenu un support natif de l'asynchronisme via les mots-clés async et await. Cela simplifie la lecture du code et élimine la "pyramide des closures".
Déclaration et appel d'une fonction asynchrone
func fetchUserData() async -> String { // Simulation d'une requête réseau try? await Task.sleep(nanoseconds: 2_000_000_000) return "Données utilisateur chargées"}
Task { let result = await fetchUserData() print(result) // Affiche "Données utilisateur chargées"}Appels parallèles avec async let
func loadImage() async -> Data { return Data() }func loadMetadata() async -> String { return "meta" }
Task { async let image = loadImage() async let metadata = loadMetadata() let result = try await (image, metadata) print("Image : \\(result.0), métadonnées : \\(result.1)")}async let permet de lancer plusieurs tâches asynchrones en parallèle et d'attendre qu'elles soient toutes terminées. C'est plus efficace qu'un appel séquentiel.
3. Actors : sécurité des threads au niveau du langage
Les acteurs (Actors) sont un nouveau mécanisme de protection contre les conditions de course. Ils garantissent que l'accès à leur état est automatiquement synchronisé.
Définition et utilisation d'un acteur
actor BankAccount { private var balance: Double = 0.0 func deposit(amount: Double) { balance += amount print("Solde après dépôt : \\(balance)") } func withdraw(amount: Double) -> Bool { guard balance >= amount else { return false } balance -= amount return true } func getBalance() -> Double { balance }}
Task { let account = BankAccount() await account.deposit(amount: 100) let success = await account.withdraw(amount: 50) print("Retrait réussi : \\(success)")}Toutes les méthodes de l'acteur sont isolées par défaut — on ne peut les appeler qu'avec await. Cela exclut toute modification accidentelle des données depuis plusieurs threads.
Méthodes non isolées (nonisolated)
actor Logger { nonisolated func log(message: String) { print("Log : \\(message)")\