# Questions entretien Swift Structured Concurrency : async/await, TaskGroup, actors > Questions techniques sur Swift Structured Concurrency en entretien : async/await, TaskGroup, actors et patterns de concurrence pour iOS 2026 - Published: 2026-02-26 - Updated: 2026-03-28 - Author: SharpSkill - Tags: swift, concurrency, async-await, actors, interview - Reading time: 12 min --- La concurrence structurée introduite dans Swift 5.5 a révolutionné la programmation asynchrone sur iOS. Les recruteurs testent désormais la maîtrise d'async/await, TaskGroup et des actors lors des entretiens techniques. Voici les questions essentielles et les réponses attendues pour briller en entretien. > **Points clés testés en entretien** > > Les recruteurs évaluent trois compétences : la compréhension des concepts fondamentaux (async/await, Task), la maîtrise des patterns de concurrence (TaskGroup, actor isolation) et la capacité à diagnostiquer les erreurs courantes (data races, deadlocks). ## Quelle est la différence entre async/await et DispatchQueue ? **Réponse attendue** : async/await offre une concurrence structurée avec un code séquentiel lisible, tandis que DispatchQueue utilise des callbacks et peut mener à du "callback hell". Swift gère automatiquement les threads avec async/await. ```swift // NetworkService.swift // Comparison: async/await vs DispatchQueue // ❌ Ancien style : DispatchQueue avec callbacks func fetchUserOld(id: Int, completion: @escaping (Result) -> Void) { DispatchQueue.global().async { // Appel réseau simulé let result = self.performNetworkRequest(id: id) DispatchQueue.main.async { completion(result) } } } // ✅ Nouveau style : async/await plus lisible func fetchUser(id: Int) async throws -> User { // Le runtime Swift gère les threads automatiquement // Pas besoin de basculer manuellement entre queues return try await performNetworkRequest(id: id) } ``` **Points clés** : async/await élimine les pyramides de callbacks, réduit les erreurs de thread (pas besoin de `DispatchQueue.main.async`) et permet au runtime Swift d'optimiser l'exécution sur les CPU cores disponibles. > **Avantage performance** > > Le runtime Swift utilise un thread pool optimisé qui évite la création excessive de threads. Contrairement à DispatchQueue où chaque `.async` peut créer un nouveau thread, async/await réutilise intelligemment les threads existants. ## Comment gérer plusieurs opérations asynchrones en parallèle ? **Réponse attendue** : Utiliser `async let` pour 2-3 tâches simples, ou `TaskGroup` pour un nombre dynamique de tâches parallèles avec collecte des résultats. ```swift // DataFetcher.swift // Parallel async operations strategies struct DataFetcher { // Stratégie 1: async let pour tâches fixes (2-4 opérations) func loadDashboard() async throws -> Dashboard { // Lancement en parallèle de 3 requêtes async let user = fetchUser() async let posts = fetchPosts() async let notifications = fetchNotifications() // Attente des résultats (en parallèle, pas séquentiel) let (userData, postsData, notificationsData) = try await (user, posts, notifications) return Dashboard(user: userData, posts: postsData, notifications: notificationsData) } // Stratégie 2: TaskGroup pour nombre dynamique de tâches func downloadImages(urls: [URL]) async throws -> [UIImage] { // TaskGroup permet de gérer N tâches avec collecte des résultats try await withThrowingTaskGroup(of: (Int, UIImage).self) { group in // Lancement d'une tâche par URL for (index, url) in urls.enumerated() { group.addTask { let (data, _) = try await URLSession.shared.data(from: url) guard let image = UIImage(data: data) else { throw ImageError.invalidData } return (index, image) // Retourne index pour conserver l'ordre } } // Collecte des résultats dans l'ordre var images = [UIImage?](repeating: nil, count: urls.count) for try await (index, image) in group { images[index] = image } return images.compactMap { $0 } } } } ``` **Erreur courante** : Utiliser `await` séquentiellement au lieu de `async let` parallélise les appels. `let user = await fetchUser(); let posts = await fetchPosts()` exécute séquentiellement (lent), tandis que `async let` lance les deux en même temps. ## Qu'est-ce qu'un actor et pourquoi l'utiliser ? **Réponse attendue** : Un actor est un type qui protège son état mutable contre les data races en garantissant un accès séquentiel. Il remplace les locks manuels (NSLock, DispatchQueue) pour sécuriser les accès concurrents. ```swift // CacheManager.swift // Actor for thread-safe state management // ❌ Classe classique : risque de data race class UnsafeCache { private var cache: [String: Data] = [:] // Pas thread-safe ! func store(_ data: Data, for key: String) { cache[key] = data // ⚠️ Race condition si accès concurrent } } // ✅ Actor : protection automatique contre les races actor SafeCache { private var cache: [String: Data] = [:] // Accès séquentiels garantis par l'actor isolation func store(_ data: Data, for key: String) { cache[key] = data // ✅ Thread-safe automatiquement } func retrieve(for key: String) -> Data? { return cache[key] // ✅ Lecture protégée } // Méthode synchrone interne (pas d'await nécessaire) nonisolated func clearAll() async { // nonisolated permet d'appeler depuis n'importe quel contexte await self.clear() } private func clear() { cache.removeAll() } } // Utilisation : await requis pour accéder à l'actor let cache = SafeCache() await cache.store(data, for: "user_123") // Await obligatoire let cachedData = await cache.retrieve(for: "user_123") ``` **Points clés** : L'actor garantit qu'un seul thread accède à son état à la fois. Le compilateur force l'utilisation d'`await` pour les appels externes, ce qui rend les potentiels suspension points explicites. > **Piège MainActor** > > `@MainActor` est un actor global pour les opérations UI. Marquer une classe `@MainActor` force toutes ses méthodes à s'exécuter sur le main thread. Attention aux appels bloquants qui peuvent figer l'interface ! ## Comment gérer les erreurs dans une TaskGroup ? **Réponse attendue** : `withThrowingTaskGroup` propage la première erreur rencontrée et annule automatiquement les tâches restantes. Pour collecter toutes les erreurs, utiliser `Result` dans le TaskGroup. ```swift // BatchProcessor.swift // Error handling strategies in TaskGroup struct BatchProcessor { // Stratégie 1: Propagation de la première erreur (échec rapide) func processItemsFastFail(items: [Item]) async throws -> [ProcessedItem] { try await withThrowingTaskGroup(of: ProcessedItem.self) { group in for item in items { group.addTask { // Si une tâche throw, le group annule les autres try await self.process(item) } } // Collecte des résultats jusqu'à la première erreur var results: [ProcessedItem] = [] for try await result in group { results.append(result) } return results } // ⚠️ Si une tâche échoue, les autres sont annulées } // Stratégie 2: Collecte de toutes les erreurs (résilience) func processItemsResilient(items: [Item]) async -> ([ProcessedItem], [Error]) { await withTaskGroup(of: Result.self) { group in for item in items { group.addTask { // Encapsulation dans Result pour capturer les erreurs do { let result = try await self.process(item) return .success(result) } catch { return .failure(error) } } } // Séparation succès/échecs var successes: [ProcessedItem] = [] var errors: [Error] = [] for await result in group { switch result { case .success(let item): successes.append(item) case .failure(let error): errors.append(error) } } return (successes, errors) } } private func process(_ item: Item) async throws -> ProcessedItem { // Traitement avec possibilité d'erreur try await Task.sleep(nanoseconds: 100_000_000) return ProcessedItem(from: item) } } ``` **Point clé** : `withThrowingTaskGroup` stoppe dès la première erreur (utile pour les opérations atomiques), tandis que `withTaskGroup` + `Result` permet de continuer malgré les erreurs (utile pour les traitements par batch). ## Quelle est la différence entre Task, Task.detached et async let ? **Réponse attendue** : `Task` hérite du contexte parent (priorité, actor isolation), `Task.detached` crée une tâche indépendante sans héritage, et `async let` crée une tâche enfant qui attend automatiquement à la fin du scope. ```swift // TaskLifecycle.swift // Understanding Task creation patterns @MainActor class ViewModel { var isLoading = false // Scénario 1: Task hérite du contexte (@MainActor ici) func loadDataWithTask() { Task { // ✅ Hérite @MainActor du parent // Pas besoin de await MainActor.run self.isLoading = true let data = try await fetchData() self.isLoading = false // ✅ Toujours sur MainActor } } // Scénario 2: Task.detached crée une tâche indépendante func loadDataDetached() { Task.detached { // ⚠️ N'hérite PAS de @MainActor let data = try await self.fetchData() // ❌ Erreur : isLoading n'est pas accessible directement // await MainActor.run { // self.isLoading = false // } } } // Scénario 3: async let crée une tâche enfant structurée func loadMultipleData() async throws { // Les tâches async let sont liées au scope actuel async let users = fetchUsers() async let posts = fetchPosts() // ⚠️ Si on quitte la fonction avant await, compilation error let (usersData, postsData) = try await (users, posts) // Les tâches async let sont automatiquement annulées // si on sort du scope (ex: throw avant le await) } private func fetchData() async throws -> Data { try await URLSession.shared.data(from: URL(string: "https://api.example.com")!).0 } private func fetchUsers() async throws -> [User] { [] } private func fetchPosts() async throws -> [Post] { [] } } ``` **Cas d'usage** : - `Task` : Opérations liées au contexte actuel (ex: mise à jour UI depuis un ViewModel) - `Task.detached` : Tâches en arrière-plan indépendantes (ex: logs, analytics) - `async let` : Opérations parallèles avec résultats nécessaires dans le scope actuel ## Comment implémenter un timeout sur une opération async ? **Réponse attendue** : Utiliser `Task.sleep` dans une race entre la tâche principale et une tâche timeout avec `withThrowingTaskGroup`, ou créer un utilitaire `withTimeout`. ```swift // AsyncTimeout.swift // Timeout implementation for async operations enum TimeoutError: Error { case timedOut } // Utilitaire générique pour ajouter un timeout func withTimeout( seconds: TimeInterval, operation: @escaping () async throws -> T ) async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in // Tâche 1: l'opération principale group.addTask { try await operation() } // Tâche 2: le timeout group.addTask { try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) throw TimeoutError.timedOut } // Première tâche qui termine gagne guard let result = try await group.next() else { throw TimeoutError.timedOut } // Annule la tâche perdante (importante pour cleanup) group.cancelAll() return result } } // Exemple d'utilisation struct NetworkService { func fetchUserWithTimeout(id: Int) async throws -> User { // Timeout de 5 secondes sur l'appel réseau try await withTimeout(seconds: 5) { try await self.fetchUser(id: id) } } private func fetchUser(id: Int) async throws -> User { let url = URL(string: "https://api.example.com/users/\(id)")! let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } } ``` **Alternative moderne** : À partir d'iOS 16, utiliser `URLSession` avec `timeoutInterval` configuré via `URLSessionConfiguration` pour les appels HTTP spécifiquement. > **Annulation explicite** > > `group.cancelAll()` est crucial pour libérer les ressources. Sans cela, la tâche perdante continuerait en arrière-plan jusqu'à sa complétion naturelle, gaspillant CPU et mémoire. ## Comment partager un état mutable entre plusieurs tasks de manière sécurisée ? **Réponse attendue** : Utiliser un `actor` pour l'état partagé, ou `AsyncStream` pour communiquer entre tasks via un flux de valeurs. ```swift // SharedStateManager.swift // Safe state sharing between concurrent tasks // Approche 1: Actor pour état partagé avec accès séquentiel actor DownloadManager { private var activeDownloads: [String: Task] = [:] private var cache: [String: Data] = [:] // Démarre un téléchargement ou retourne la tâche existante func download(url: String) async throws -> Data { // Vérifie le cache d'abord if let cachedData = cache[url] { return cachedData } // Vérifie si un téléchargement est déjà en cours if let existingTask = activeDownloads[url] { return try await existingTask.value } // Crée une nouvelle tâche de téléchargement let task = Task { let data = try await self.performDownload(url: url) // Mise à jour du cache (thread-safe via actor) await self.completeDownload(url: url, data: data) return data } activeDownloads[url] = task return try await task.value } private func performDownload(url: String) async throws -> Data { let urlObject = URL(string: url)! let (data, _) = try await URLSession.shared.data(from: urlObject) return data } private func completeDownload(url: String, data: Data) { cache[url] = data activeDownloads.removeValue(forKey: url) } } // Approche 2: AsyncStream pour communication inter-tasks struct EventStream { private let continuation: AsyncStream.Continuation let stream: AsyncStream init() { var continuation: AsyncStream.Continuation! stream = AsyncStream { cont in continuation = cont } self.continuation = continuation } func emit(_ event: Event) { continuation.yield(event) } func finish() { continuation.finish() } } // Exemple: monitoring de progression partagée func processItemsWithProgress(items: [Item]) async { let eventStream = EventStream() // Task 1: Traitement des items Task { for item in items { await processItem(item) eventStream.emit(.itemProcessed(item.id)) } eventStream.finish() } // Task 2: Mise à jour UI avec la progression Task { @MainActor in for await event in eventStream.stream { switch event { case .itemProcessed(let id): print("Item \(id) processed") } } } } enum Event { case itemProcessed(String) } ``` **Choix architecture** : Actor pour état centralisé avec logique métier, AsyncStream pour communication événementielle entre composants découplés. ## Qu'est-ce que le Task cancellation et comment le gérer ? **Réponse attendue** : Le Task cancellation permet d'annuler des opérations asynchrones en cours. Les tasks doivent vérifier périodiquement `Task.isCancelled` ou utiliser `Task.checkCancellation()` qui throw une erreur. ```swift // CancellableOperations.swift // Implementing proper task cancellation struct ImageProcessor { // Traitement annulable avec vérifications explicites func processImages(_ images: [UIImage]) async throws -> [ProcessedImage] { var results: [ProcessedImage] = [] for (index, image) in images.enumerated() { // Vérification 1: Check booléen (continue ou skip) if Task.isCancelled { print("Cancelled after \(index) images") break // Arrêt graceful } let processed = try await processImage(image) results.append(processed) // Vérification 2: Throw automatique si annulé try Task.checkCancellation() } return results } private func processImage(_ image: UIImage) async throws -> ProcessedImage { // Simulation de traitement long for _ in 0..<10 { try await Task.sleep(nanoseconds: 100_000_000) // ✅ Vérifie l'annulation dans les boucles longues try Task.checkCancellation() } return ProcessedImage(from: image) } } // SwiftUI: Annulation automatique lors de la disparition de la vue struct ImageGalleryView: View { @State private var images: [ProcessedImage] = [] var body: some View { ScrollView { // Affichage des images } .task { // ✅ Task annulée automatiquement quand la vue disparaît let processor = ImageProcessor() do { images = try await processor.processImages(sourceImages) } catch is CancellationError { print("Processing cancelled") } } } } // Annulation manuelle d'une task stockée class DownloadViewModel { private var downloadTask: Task? func startDownload() { downloadTask = Task { do { try await performLongDownload() } catch is CancellationError { print("Download cancelled by user") } } } func cancelDownload() { // Annulation explicite de la task stockée downloadTask?.cancel() downloadTask = nil } private func performLongDownload() async throws { try Task.checkCancellation() // Logique de téléchargement } } ``` **Points clés** : - `Task.isCancelled` : vérification non-bloquante (retourne bool) - `Task.checkCancellation()` : throw `CancellationError` si annulé - `.task { }` modifier SwiftUI : annulation automatique à la disparition de la vue > **Annulation coopérative** > > Swift utilise un modèle d'annulation coopératif : les tasks ne sont pas tuées de force. Le code doit vérifier activement `Task.isCancelled` ou `checkCancellation()` pour réagir à l'annulation. Sans ces vérifications, la task continue indéfiniment. ## Comment utiliser MainActor correctement dans une app SwiftUI ? **Réponse attendue** : Annoter les ViewModels `@MainActor` pour garantir que toutes les mises à jour d'état UI se font sur le main thread. Utiliser `@MainActor` sur les fonctions individuelles si seulement certaines opérations touchent l'UI. ```swift // MainActorPatterns.swift // Proper MainActor usage in SwiftUI architecture // Pattern 1: ViewModel entièrement @MainActor @MainActor class UserViewModel: ObservableObject { @Published var user: User? @Published var isLoading = false @Published var errorMessage: String? private let repository: UserRepository init(repository: UserRepository) { self.repository = repository } // ✅ Toutes les méthodes sont implicitement @MainActor func loadUser(id: Int) async { isLoading = true // Pas besoin de await ou MainActor.run errorMessage = nil do { // L'appel réseau est fait sur un background thread par le runtime user = try await repository.fetchUser(id: id) } catch { errorMessage = error.localizedDescription } isLoading = false // Toujours sur MainActor } // Méthode synchrone aussi sur MainActor func clearUser() { user = nil errorMessage = nil } } // Pattern 2: Méthodes sélectives avec @MainActor class DataSyncService { // ❌ Pas @MainActor sur la classe (pas d'UI ici) func syncData() async throws { // Traitement en background let data = try await fetchRemoteData() let processed = processData(data) // ✅ Bascule sur MainActor uniquement pour l'UI await updateUI(with: processed) } @MainActor private func updateUI(with data: ProcessedData) { // Mise à jour d'une property observable NotificationCenter.default.post( name: .dataDidSync, object: data ) } // Background work (pas @MainActor) private func fetchRemoteData() async throws -> Data { // Appel réseau Data() } private func processData(_ data: Data) -> ProcessedData { // Traitement CPU-intensif en background ProcessedData() } } // Pattern 3: Annotation de closures class ImageLoader { func loadImage(url: URL, completion: @MainActor @escaping (UIImage?) -> Void) async { let image = try? await downloadImage(from: url) // ✅ Completion est garantie sur MainActor await completion(image) } private func downloadImage(from url: URL) async throws -> UIImage { let (data, _) = try await URLSession.shared.data(from: url) return UIImage(data: data) ?? UIImage() } } ``` **Erreur courante** : Marquer toute la classe `@MainActor` alors que seules certaines méthodes touchent l'UI. Cela force tout le code sur le main thread, même les opérations lourdes qui devraient être en background. ## Comment gérer les data races avec Sendable ? **Réponse attendue** : Le protocole `Sendable` garantit qu'un type peut être partagé entre tasks sans risque de data race. Les types valeurs (struct, enum) sont automatiquement `Sendable`, les classes doivent être `final` avec des propriétés immuables ou protégées. ```swift // SendableCompliance.swift // Making types safe for concurrent access // ✅ Struct : automatiquement Sendable (type valeur) struct UserData: Sendable { let id: Int let name: String let email: String } // ✅ Enum : automatiquement Sendable enum LoadingState: Sendable { case idle case loading case loaded(UserData) case failed(Error) // ⚠️ Error doit aussi être Sendable } // ❌ Classe avec état mutable : pas Sendable par défaut class UnsafeCounter { var count = 0 // Mutable, non protégé func increment() { count += 1 // Data race possible } } // ✅ Classe immutable : Sendable explicite final class SafeConfig: @unchecked Sendable { let apiKey: String let timeout: TimeInterval init(apiKey: String, timeout: TimeInterval) { self.apiKey = apiKey self.timeout = timeout } } // ✅ Classe avec état protégé par actor actor SafeCounter: Sendable { private var count = 0 // Protégé par actor isolation func increment() { count += 1 // Thread-safe automatiquement } func getValue() -> Int { return count } } // ✅ Classe avec état protégé manuellement final class ThreadSafeCache: @unchecked Sendable { private let lock = NSLock() private var storage: [String: Data] = [:] func store(_ data: Data, for key: String) { lock.lock() defer { lock.unlock() } storage[key] = data } func retrieve(for key: String) -> Data? { lock.lock() defer { lock.unlock() } return storage[key] } } // Utilisation : le compilateur vérifie Sendable func processInBackground(data: UserData) { // ✅ UserData est Sendable Task.detached { // Pas de warning : UserData est un type valeur Sendable print("Processing user: \(data.name)") } } func processUnsafe(counter: UnsafeCounter) { Task.detached { // ⚠️ Warning : UnsafeCounter n'est pas Sendable // counter.increment() } } ``` **Règles Sendable** : - Struct/Enum avec propriétés Sendable : automatiquement Sendable - Classes : doivent être `final` + immutables, ou utiliser `@unchecked Sendable` avec protection manuelle (locks, actors) - Closures : automatiquement Sendable si capturent uniquement des types Sendable > **@unchecked Sendable** > > `@unchecked Sendable` désactive les vérifications du compilateur. À utiliser uniquement si la thread-safety est garantie manuellement (locks, serial queues). Responsabilité du développeur d'éviter les data races. ## Conclusion La maîtrise de Swift Structured Concurrency est devenue incontournable pour les entretiens iOS en 2026. Les recruteurs testent trois niveaux : compréhension des concepts (async/await vs callbacks), maîtrise des patterns (TaskGroup, actor isolation) et debugging (cancellation, Sendable). **Checklist de préparation** : - ✅ Expliquer async/await vs DispatchQueue avec un exemple concret - ✅ Démontrer l'utilisation de TaskGroup pour des opérations parallèles - ✅ Implémenter un actor thread-safe pour protéger un état mutable - ✅ Gérer les erreurs dans un contexte concurrent (Result, throwing) - ✅ Différencier Task, Task.detached et async let avec leurs cas d'usage - ✅ Implémenter un timeout sur une opération asynchrone - ✅ Utiliser MainActor correctement dans une architecture SwiftUI - ✅ Comprendre Sendable et éviter les data races Les meilleurs candidats combinent théorie et pratique : expliquer le "pourquoi" (éviter les data races, améliorer la lisibilité) et le "comment" (code fonctionnel avec gestion d'erreurs). Entraînez-vous sur des projets réels pour consolider ces patterns. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ios/questions-entretien-swift-structured-concurrency-async-await-taskgroup-actors