Concurrence et multithreading en Swift : niveau avancé
\n\nLes applications modernes pour iOS et macOS nécessitent des performances élevées et une interface réactive. Le travail avancé avec la concurrence en Swift va bien au-delà de la simple utilisation de Grand Central Dispatch (GCD). Dans cet article, nous examinerons les outils clés de Swift 5.5+ pour la gestion des tâches asynchrones : async/await, Actors, TaskGroup et le protocole Sendable. Vous apprendrez à éviter les conditions de course, à structurer correctement le code multithread et à optimiser les performances.
1. Bases de async/await et Structured Concurrency
\n\nLes mots-clés async et await sont le fondement de la concurrence moderne en Swift. Ils permettent d'écrire du code asynchrone qui ressemble à du code synchrone, ce qui simplifie grandement la lecture et le débogage.
Déclaration d'une fonction asynchrone :
\nfunc fetchUserData() async throws -> User {\n let url = URL(string: "https://api.example.com/user")!\n let (data, _) = try await URLSession.shared.data(from: url)\n let decoder = JSONDecoder()\n return try decoder.decode(User.self, from: data)\n}\n\nAppel d'une fonction asynchrone avec gestion des erreurs :
\nTask {\n do {\n let user = try await fetchUserData()\n print("User name: \\(user.name)")\n } catch {\n print("Failed to fetch user: \\(error)")\n }\n}\n\nPoints importants :
\n- \n
- Structured Concurrency : Chaque tâche asynchrone a une durée de vie claire, limitée à la portée du
Taskparent. \n - Annulation des tâches : Annulation automatique des tâches enfants lors de l'annulation du Task parent. \n
- Priorités : Utilisez
Task(priority: .userInitiated)pour les tâches importantes pour l'utilisateur. \n
2. Actors : protection contre les conditions de course
\n\nAvant Swift 5.5, les développeurs utilisaient souvent des files d'attente GCD sérialisées pour protéger l'état partagé. Les Actors fournissent une isolation intégrée : l'accès à l'état d'un actor n'est possible que via await, ce qui garantit la sécurité des threads au niveau du compilateur.
Exemple d'Actor pour un compte bancaire :
\nactor BankAccount {\n private var balance: Double = 0.0\n\n func deposit(amount: Double) {\n balance += amount\n }\n\n func withdraw(amount: Double) throws {\n guard balance >= amount else {\n throw BankError.insufficientFunds\n }\n balance -= amount\n }\n\n func getBalance() -> Double {\n return balance\n }\n}\n\n// Utilisation\nlet account = BankAccount()\nTask {\n await account.deposit(amount: 100.0)\n let currentBalance = await account.getBalance()\n print("Balance: \\(currentBalance)")\n}\n\nAvantages des Actors :
\n- \n
- Vérification par le compilateur : Toute tentative d'accès à une propriété d'un actor sans
awaitprovoquera une erreur de compilation. \n - Réentrance (Reentrancy) : Un actor peut suspendre l'exécution de la méthode en cours (par exemple, lors d'un
awaità l'intérieur de l'actor) et traiter un autre appel, ce qui empêche les interblocages. \n - Performances : Les Actors utilisent des mécanismes de synchronisation optimisés, souvent plus rapides que les verrous manuels. \n
3. TaskGroup : traitement parallèle des données
\n\nLorsque vous devez exécuter plusieurs opérations asynchrones indépendantes et attendre leur achèvement, TaskGroup est l'outil idéal. Il gère automatiquement le nombre de tâches parallèles et collecte les résultats.
Exemple de téléchargement parallèle de plusieurs images :
\nfunc downloadImages(urls: [URL])