Aujourd’hui, nous allons explorer les Swift Actors. Pour commencer, comme d’habitude, je vais vous expliquer en dĂ©tail comment les choses fonctionnent sous le capot. En effet, Swift 5.5 a introduit les Actors dans le cadre de son modèle de concurrence, principalement afin de prĂ©venir les courses de donnĂ©es en garantissant un accès sĂ©curisĂ© aux Ă©tats mutables partagĂ©s. C’est pourquoi, dans cet article, vous allez dĂ©couvrir en profondeur non seulement les Actors eux mĂŞmes, mais aussi les GlobalActors, le MainActor, ainsi que tout leur Ă©cosystème. Enfin, nous examinerons de plus comment le compilateur Swift, LLVM et le modèle de concurrence de Swift les gèrent en interne.
Avant même de commencer à écrire cet article, de nombreuses questions me trottaient dans la tête, et elles ont vraiment éveillé ma curiosité :
- Que sont les Actors et comment fonctionnent-ils en interne?
- Comment les Actors garantissent-ils l’isolation et empêchent-ils les courses de données ?
- Quel est le rĂ´le des GlobalActors et du MainActor?
- Comment le compilateur Swift gère-t-il la concurrence basée sur les Actors?
- Quelle sont les bonnes pratiques, les pièges à éviter?
1. Que sont les Actors et comment fonctionnent-ils en interne?
Tout d’abord, un Actor en Swift est un type de référence, similaire à une classe, qui garantit un accès thread-safe à son état mutable en sérialisant les tâches via un exécuteur interne (queue / une file d’attente). Ainsi, il empêche les races conditions de concurrence en permettant à une seule tâche de modifier son état à la fois. En revanche, contrairement aux locks (verrous), les Actors utilisent la concurrence structurée, ce qui implique l’utilisation d’un await
pour un accès sécurisé. Par ailleurs, lorsqu’une fonction asynchrone est suspendue, l’Actor devient réentrant, permettant ainsi à d’autres tâches de s’exécuter pendant ce temps. Enfin, en interne, le runtime de Swift planifie les tâches de manière efficace, non seulement en garantissant une concurrence sûre, mais aussi une optimisation sans synchronisation manuelle.
1.1 Syntaxe et Bases des Actors
actor BankManager {
var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
}
func getBalance() -> Double {
return balance
}
}
Propriétés clés des Actors
✔️ Isolation: Une seule tâche peut accĂ©der Ă l’Ă©tat mutable Ă la fois.
✔️ Concurrency-safe: Empêche les courses de données (data race) sans nécessiter de verrous (locks).
✔️ Reference Type: Comme une classe, mais il impose l’isolation.
1.2 Comment appeler les mĂ©thodes d’un Actor
Comme les Actors sont isolĂ©s, l’appel de leurs mĂ©thodes depuis l’extĂ©rieur nĂ©cessite l’utilisation de await pour une exĂ©cution asynchrone :
let manager = BankManager()
Task {
await manager.deposit(amount: 100.0)
print(await manager.getBalance()) // 100.0
}
💡 À retenir:
- L’appel direct des mĂ©thodes d’un Actor nĂ©cessite await, car elles s’exĂ©cutent de manière asynchrone.
- Les propriétés Immutable peuvent être accédées de manière synchrone.:
actor Counter {
let name: String = "MyCounter"
}
let counter = Counter()
print(counter.name) // âś… Allowed, because `name` is immutable.
2. Comment les Actors garantissent-ils l’isolation et empĂŞchent les courses de donnĂ©es (Data Race) ?
Tout d’abord, les Actors garantissent l’isolation et empêchent les courses de données en imposant un accès exclusif à leur état mutable. Concrètement, en interne, chaque Actor dispose d’un exécuteur dédié (queue) qui sérialise les tâches, permettant à une seule opération de modifier ses propriétés à la fois. Cependant, l’accès direct à l’état d’un Actor depuis l’extérieur est interdit, ce qui oblige à utiliser await
 pour une interaction contrôlée. Ainsi, cela empêche plusieurs threads de lire et d’écrire simultanément, éliminant de ce fait les conditions de concurrence. Par ailleurs, la réentrance des Actors permet à d’autres tâches de s’exécuter pendant qu’un Actor est en attente d’une opération await
, assurant non seulement une efficacité, mais aussi une sécurité préservée.
2.1 Code non sécurisé:
class UnsafeCounter {
var value = 0
}
let counter = UnsafeCounter()
DispatchQueue.concurrentPerform(iterations: 10) {
counter.value += 1 // 🚨 Data race
}
💥 Le code ci-dessus est non sécurisé car plusieurs threads peuvent accéder à valeur
simultanément.
2.2 Comment les Actors garantissent la sécurité:
actor SafeCounter {
var value = 0
func increment() {
value += 1
}
}
let counter = SafeCounter()
Task {
await counter.increment() // âś… Safe
}
Pourquoi est-ce sécurisé ?
Parce que Swift garantit que une seule tâche à la fois peut accéder à valeur
.
3. Quel est le rĂ´le des GlobalActors et du MainActor ?
En Swift, GlobalActor
 et @MainActor
 sont utilisĂ©s pour gĂ©rer la concurrence en garantissant que des morceaux de code spĂ©cifiques s’exĂ©cutent sur un Actor dĂ©signĂ©, aidant ainsi Ă prĂ©venir les courses de donnĂ©es.
3.1 Que sont les GlobalActor ?
En substance, un GlobalActor
est un type spécial d’Actor qui synchronise systématiquement l’accès à travers plusieurs instances. Plus précisément, il permet de définir un Actor singleton qui fournit un contexte d’exécution unifié pour des fonctions, propriétés ou types spécifiques. En pratique, vous pouvez l’utiliser pour regrouper du code lié qui nécessite impérativement de s’exécuter sur le même Actor, notamment pour éviter les conflits de concurrence ou centraliser des ressources partagées.
@globalActor
struct MyGlobalActor {
static let shared = MyActor()
}
actor MyActor {
func doWork() {
print("Executing on MyActor")
}
}
@MyGlobalActor
func someFunction() {
print("This function runs on MyGlobalActor")
}
Commençons par MyGlobalActor
: il s’agit d’un GlobalActor personnalisé. En effet, toute fonction, propriété ou type annoté avec @MyGlobalActor
s’exécutera automatiquement dans le contexte de MyActor
. Cependant, il existe une limitation inhérente à sa conception (sans pour autant être un défaut) : un GlobalActor doit impérativement avoir une seule et unique instance partagée shared
. Plus précisément, vous ne pouvez pas déclarer plusieurs instances shared
au sein d’un @globalActor
. Par conséquent, le compilateur rejettera ce code, car il ne saura pas quel Actor prioriser dans ce cas.
3.2 Qu’est-ce qu’un MainActor ?
En premier lieu, le @MainActor
est un global actor intégré qui garantit que le code annoté s’exécute exclusivement sur le thread principal. Cette particularité le rend indispensable pour mettre à jour l’interface utilisateur, que ce soit dans des applications SwiftUI ou UIKit. Concrètement, vous pouvez l’utiliser dans deux cas principaux : soit pour gérer une logique spécifique liée à la vue, soit pour vous assurer qu’un code critique (comme des opérations de rendu) s’exécute de manière synchrone sur le thread principal.
@MainActor
class ViewModel {
var text: String = "Loading..."
func updateText() {
text = "Updated!"
}
}
Équivalent à :
class ViewModel {
@MainActor var text: String = "Loading..."
@MainActor func updateText() {
text = "Updated!"
}
}
Différences clés entre GlobalActor
et @MainActor
Fonctionnalité | GlobalActor | @MainActor |
---|---|---|
Objectif | DĂ©finit un contexte d’exĂ©cution global personnalisĂ© | Garantit l’exĂ©cution sur le thread principal |
Personnalisation | Oui, vous pouvez créer plusieurs GlobalActors | Non, il est prédéfini |
Cas d’utilisation | Appliquer un ordre d’exĂ©cution pour les ressources partagĂ©es. | Mises Ă jour de l’interface utilisateur et opĂ©rations rĂ©servĂ©es au thread principal |
4. Comment le compilateur Swift gère-t-il la concurrence basée sur les Actors?
4.1 Le rĂ´le de swiftc
(compilateur Swift) dans l’isolation des Actors
Le compilateur Swift (swiftc
) applique la concurrence basée sur les Actors en garantissant que tous les accès à l’état isolé d’un Actor sont correctement synchronisés. Pour ce faire, il s’appuie sur les règles d’isolation des Actors, un mécanisme qui empêche les courses de données tout en imposant une concurrence structurée. En pratique, lorsque vous marquez un type avec actor
, le compilateur restreint automatiquement l’accès direct à son état mutable depuis l’extérieur. Par ailleurs, il détecte et traite les fonctions nonisolated
, leur offrant ainsi la possibilité de s’exécuter hors du contexte d’exécution de l’Actor, si nécessaire. Enfin, grâce à une analyse statique rigoureuse, swiftc
 assure qu’un seul contexte d’exécution modifie l’état de l’Actor à la fois, ce qui rend la concurrence à la fois plus sûre et plus prévisible.
Lorsque vous écrivez :
actor MyActor {
var count = 0
func increment() {
count += 1
}
}
Le compilateur Swift swiftc le transforme en quelque chose comme ceci :
class MyActor : public swift::Actor {
private:
int count;
public:
void increment() {
// Implicitly async-safe
count++;
}
}
Voici ce qui se passe étape par étape :
- Analyse d’isolation des Actors : Le compilateur s’assure que tous les accès aux propriĂ©tĂ©s mutables d’un Actor passent par des barrières asynchrones..
- Vérification de Sendable: Le compilateur impose que les états des Actors respectent le protocole Sendable pour un accès sécurisé entre threads.
- Modèle de passage de messages: En arrière-plan, les Actors utilisent un modèle de file d’exĂ©cution (run queue) similaire Ă une boucle d’Ă©vĂ©nements (event loop).
4.2 Modèle d’exĂ©cution des Actors dans LLVM
Le modèle des Actors Swift repose sur un système d’exĂ©cution hautement optimisĂ©, intĂ©grĂ© Ă LLVM, pour gĂ©rer la concurrence avec une efficacitĂ© maximale. Contrairement aux mĂ©canismes traditionnels de verrouillage/dĂ©verrouillage, ils adoptent plutĂ´t un modèle de passage de messages similaire Ă Erlang ou aux canaux en Go qui assure qu’une seule tâche s’exĂ©cute Ă la fois au sein d’un Actor. Concrètement, chaque Actor dispose d’une file de tâches (Job Queue), organisĂ©e en FIFO (First-In, First-Out), garantissant un traitement sĂ©quentiel et ordonnĂ©. C’est prĂ©cisĂ©ment cette architecture qui Ă©limine les risques de courses de donnĂ©es.
Lorsqu’une fonction asynchrone dans un Actor appelle await
, Swift active alors des continuations des blocs logiques s’appuyant sur les coroutines LLVM pour suspendre la tâche sans bloquer le thread. Une fois l’opĂ©ration attendue terminĂ©e, la tâche reprend exactement oĂą elle s’Ă©tait interrompue. C’est dans ce mĂ©canisme que rĂ©side la puissance et l’efficacitĂ© des Actors Swift : ils combinent suspension non-bloquante et reprise contextuelle, offrant ainsi une concurrence Ă la fois sĂ»re et performante.
- Files de tâches (Job Queues) : Une file FIFO qui exécute les tâches une par une.
- Continuations : Swift utilise des coroutines basées sur LLVM (async/await) pour suspendre et reprendre les tâches.
- Verrous d’Actors ? Non ! Contrairement aux verrous traditionnels, les Actors Swift utilisent le passage de messages.
5. Quelle sont les bonnes pratiques, les pièges à éviter?
Si les Actors Swift contribuent efficacement à la sécurité en matière de concurrence, ils ne constituent pas pour autant une solution universelle. En effet, leur utilisation systématique peut entraîner des surcharges inutiles, notamment dans des scénarios à faible contention ou pour des opérations purement synchrones. C’est pourquoi comprendre quand les employer par exemple pour isoler un état mutable complexe et comment ils impactent les performances comme le coût de la sérialisation des tâches s’avère crucial pour écrire du code à la fois sûr et performant.
5.1 Quand utiliser les Actors ?
En résumé, les Actors s’avèrent particulièrement utiles lorsque plusieurs composants de votre application doivent accéder et modifier de manière sécurisée les mêmes données. En effet, comme mentionné précédemment, ils éliminent les conditions de concurrence en garantissant systématiquement qu’une seule tâche modifie les données à un instant donné. Ils sont particulièrement adaptés pour gérer des états mutables partagés (ex : configurations dynamiques), optimiser les caches, ou encore remplacer des mécanismes de verrouillage complexes (comme les NSLock
ou DispatchSemaphore
). Grâce à cette approche, vous simplifiez non seulement l’architecture concurrente, mais vous réduisez aussi les risques d’erreurs subtiles liées aux accès parallèles non contrôlés.
Scénario | Pourquoi ? |
---|---|
ProtĂ©ger l’Ă©tat mutable partagĂ© | EmpĂŞcher les conditions de course |
Gérer les données en concurrence caches | Garantir la cohérence et éviter la corruption |
Éviter les verrous DispatchQueue | Plus sûr et plus simple que la synchronisation manuelle |
5.2 Quand les Actors sont-ils inutiles ?
Bien que les Actors garantissent la sécurité concurrentielle, ils introduisent néanmoins une surcharge, ce qui les rend contre-productifs dans des contextes où les performances sont critiques ou lorsque les données sont déjà immuables. Dans ces cas précis, privilégier une structure struct
qui peut remplir le mĂŞme rĂ´le sans exiger de synchronisation.
Scénario | Meilleure alternative |
---|---|
Opérations critiques pour la performance | Accès direct (sans actor) |
Données immuables | Utiliser une structure (struct) |
Pas besoin de coordination globale | Utiliser des variables locales |
5.3 Considérations de performance
Swift’s Actors fonctionnent un model de par passage de messages, ce qui signifie que chaque tâche doit attendre son tour dans une file d’attente. Toutefois, les appels frĂ©quents entre Actors ralentissent les performances. Dans ce cas, une meilleure approche consiste Ă stocker les donnĂ©es frĂ©quemment accĂ©dĂ©es Ă l’intĂ©rieur de l’Actor, ce qui rĂ©duit la communication inutile.
5.3.1 Mauvaise approche (Appels entre Actors inefficaces)
actor InvoiceGenerator {
func generateInvoice(for userId: String) async -> Invoice {
// ❌ Cross-actor call 1
let userDetails = await fetchUserDetails(userId: userId)
// ❌ Cross-actor call 2
let purchaseHistory = await fetchPurchaseHistory(userId: userId)
return Invoice(user: userDetails, purchases: purchaseHistory)
}
}
func processInvoices(userIds: [String]) async {
let generator = InvoiceGenerator()
for userId in userIds {
// ❌ Multiple calls to actor
let invoice = await generator.generateInvoice(for: userId)
print("Generated invoice for \(invoice.user.name)")
}
}
5.3.2 Meilleure approche (Regroupement des requĂŞtes Ă l’intĂ©rieur de l’Actor)
actor InvoiceGenerator {
func generateInvoices(for userIds: [String]) async -> [Invoice] {
// âś… Batch fetch
let userDetails = await getMultipleUserDetails(userIds: userIds)
// âś… Batch fetch
let purchasesList = await getMultiplePurchases(userIds: userIds)
return userIds.compactMap { id in
guard let user = userDetails[id],
let purchases = purchasesList[id]
else { return nil }
return Invoice(user: user, purchases: purchases)
}
}
}
func processInvoices(userIds: [String]) async {
let generator = InvoiceGenerator()
// âś… One call instead of multiple
let invoices = await generator.generateInvoices(for: userIds)
for invoice in invoices {
print("Generated invoice for \(invoice.user.name)")
}
}
Pourquoi est-ce mieux ?
- Un seul appel entre Actors est effectué, réduisant la surcharge.
- Les profils mis en cache sont accédés localement, évitant un travail inutile.
- Les profils manquants sont récupérés en un seul lot, améliorant les performances.
Conclusion
En résumé, les Actors empêchent efficacement les conditions de concurrence, mais leur utilisation doit rester judicieuse pour ne pas compromettre les performances. Pour cela, il est crucial de conserver les données fréquemment sollicitées au sein de l’Actor lui-même, tout en minimisant les interactions entre différents Actors. En parallèle, privilégiez les GlobalActors lorsque certaines tâches nécessitent systématiquement le même contexte d’exécution. En suivant ces principes, vous pourrez développer un code concurrent à la fois sûr et optimisé en Swift.
Cas d’utilisation | Acteur recommandĂ© |
---|---|
Mises Ă jour de l’interface utilisateur dans SwiftUI | @MainActor |
Traitement en arrière-plan | actor Régulier |
Logging, état global | @GlobalActor Personnalisé |
Sources
Pour approfondir votre compréhension des Actors et de la concurrence en Swift, vous pouvez explorer les ressources suivantes :
- La documentation officielle de Swift sur la concurrence fournit un aperçu complet des Actors, async/await et de la concurrence structurée.
- La proposition Swift Evolution pour les Actors (SE-0306) explique la conception et les dĂ©tails d’implĂ©mentation du modèle d’Actors de Swift.
- Apprenez-en plus sur les mĂ©canismes sous-jacents dans la documentation LLVM Coroutines, qui alimente l’async/await et la suspension des tâches en Swift.
- Pour une explication visuelle et dĂ©taillĂ©e, regardez la vidĂ©o WWDC 2021 sur la concurrence en Swift, oĂą les ingĂ©nieurs d’Apple expliquent comment les Actors et async/await fonctionnent ensemble.