Comprendre les Actors en Swift

Publié le · Mis à jour le · 22 min

Wlad
Wlad
Fondateur & Tech Lead Swift

Les Actors sont au cœur du modèle de concurrence de Swift. Introduits avec Swift 5.5, ils résolvent élégamment le problème des data races en isolant l'état mutable. Avec Swift 6.2, leur utilisation devient encore plus intuitive grâce à MainActor par défaut. Ce guide couvre tout ce que vous devez savoir pour les maîtriser.

Le problème des data races

Avant les Actors, protéger l'état partagé nécessitait des locks, des queues, ou des semaphores. C'était verbeux et source d'erreurs :

Ce code fonctionne, mais le compilateur ne peut pas vérifier qu'on n'a pas oublié d'utiliser la queue quelque part. Une seule omission suffit pour créer un data race.

Actor : l'isolation garantie par le compilateur

Un Actor est un type référence qui isole automatiquement son état. Le compilateur garantit qu'un seul appel peut accéder à l'état à la fois :

Plus de queue, plus de lock. Le compilateur s'occupe de tout. Chaque Actor maintient une file d'attente interne (executor) qui sérialise les accès.

Accès aux Actors : await obligatoire

Depuis l'extérieur d'un Actor, chaque accès à une propriété ou méthode isolée nécessite await :

Le mot-clé await indique un point de suspension potentiel. Votre code attend son tour dans la file de l'Actor avant d'accéder à l'état.

Depuis l'intérieur de l'Actor

À l'intérieur de l'Actor, vous accédez directement aux propriétés et méthodes sans await :

nonisolated : opt-out de l'isolation

Certaines propriétés ou méthodes n'ont pas besoin d'isolation. Utilisez nonisolated pour les exempter :

Les constantes let sont automatiquement nonisolated car elles sont immutables et donc thread-safe.

nonisolated pour les conformances de protocoles

Un cas fréquent : conformer un Actor à un protocole synchrone comme CustomStringConvertible :

MainActor : l'Actor du thread principal

MainActor est un global actor prédéfini qui représente le thread principal. Tout le code UI doit s'exécuter dessus :

Avec @MainActor sur la classe, toutes les propriétés et méthodes s'exécutent sur le thread principal. Les @Published peuvent être modifiées en toute sécurité.

MainActor.run pour les blocs ponctuels

Si vous n'avez qu'une petite portion de code à exécuter sur le MainActor :

MainActor.assumeIsolated pour les callbacks

Quand vous savez qu'un callback sera appelé sur le main thread mais que le compilateur ne peut pas le vérifier :

Attention : assumeIsolated crashe si vous n'êtes pas vraiment sur le MainActor au runtime.

Global Actors personnalisés

Vous pouvez créer vos propres global actors pour isoler des ressources spécifiques :

Tous les accès à FileManager sont maintenant sérialisés sur le DiskActor, évitant les conflits d'accès fichier.

Quand utiliser un Global Actor vs un Actor standard

SituationChoix recommandé

Instance unique partagée

Global Actor

Plusieurs instances indépendantes

Actor standard

Coordination UI

MainActor

Ressource système unique (DB, fichiers)

Global Actor personnalisé

État local à une feature

Actor standard

Sendable : traverser les frontières d'isolation

Quand des données traversent les frontières d'un Actor, elles doivent être Sendable. Le protocole Sendable garantit qu'un type peut être partagé entre contextes concurrents sans data race.

Types automatiquement Sendable

Certains types sont Sendable par défaut :

Conformance explicite pour les classes

Les classes nécessitent une attention particulière :

@unchecked Sendable : à utiliser avec précaution

Pour les types avec synchronisation interne que le compilateur ne peut pas vérifier :

Attention : @unchecked Sendable désactive les vérifications du compilateur. Vous êtes responsable de la thread-safety. Préférez Mutex (iOS 18+) quand possible :

@Sendable pour les closures

Les closures passées entre contextes concurrents doivent être @Sendable :

Swift 6.2 : MainActor par défaut

Swift 6.2 introduit defaultIsolation qui change fondamentalement l'approche :

Avec ce réglage, tout code sans annotation explicite s'exécute sur le MainActor :

@concurrent pour le travail parallèle

Si vous voulez explicitement quitter le MainActor :

Conformances isolées (SE-0470)

Swift 6.2 permet de restreindre une conformance de protocole à un acteur :

Sans @MainActor Equatable, le compilateur pourrait exécuter == sur n'importe quel thread, violant l'isolation.

Réentrance des Actors

Un point crucial : les Actors sont réentrants. Quand vous faites un await à l'intérieur d'un Actor, d'autres appels peuvent s'exécuter pendant l'attente :

Pattern pour éviter les problèmes de réentrance

Capturez les valeurs nécessaires avant l'await :

Ou utilisez une approche transactionnelle :

Patterns d'architecture avec Actors

Repository Actor

ViewModel avec Actor interne

Bonnes pratiques

1. Préférer les Actors aux locks manuels

2. Minimiser les await dans les Actors

Chaque await est un point où l'état peut changer. Regroupez les opérations :

3. Garder les Actors focalisés

Un Actor devrait avoir une responsabilité unique :

Pièges courants

1. Actor hopping excessif

Passer d'un Actor à l'autre a un coût :

2. Deadlocks avec appels synchrones

Les Actors n'ont que des méthodes async vers l'extérieur. Attention aux designs qui créent des dépendances circulaires :

3. Oublier que les propriétés computed sont isolées

Pour aller plus loin

Les Actors sont la base de la concurrence sûre en Swift. Maîtrisez-les avant de passer aux concepts avancés :

  • TaskGroup pour le parallélisme dynamique

  • AsyncStream pour les flux de données asynchrones

  • Custom Executors pour contrôler l'exécution

Ressources officielles