Réponse à la question.
Avant Swift 5.5, la concurrence reposait sur Grand Central Dispatch (GCD) et la gestion manuelle des threads, ce qui entraînait fréquemment des courses de données et une corruption de la mémoire en raison d'un état mutable partagé non protégé. Swift a introduit la concurrence structurée avec les Actors pour fournir des garanties d'isolation, mais le compilateur avait besoin d'un mécanisme pour s'assurer que les valeurs passées entre ces domaines isolés étaient intrinsèquement sûres pour les threads. Cela a conduit au protocole Sendable, qui marque les types comme sûrs à partager à travers des frontières concurrentes en imposant des sémantiques de valeur ou une synchronisation interne au niveau du type.
Lorsqu'un Actor reçoit une valeur de l'extérieur de son domaine d'isolation, cette valeur pourrait potentiellement être un type de référence partagé avec d'autres contextes d'exécution, permettant des mutations simultanées qui violent la sécurité de la mémoire. Les approches traditionnelles s'appuient sur des verrous à l'exécution ou des mutex pour protéger les sections critiques, mais celles-ci introduisent des frais généraux, des risques de blocage, et sont sujettes à des erreurs humaines durant leur mise en œuvre. Le défi était de concevoir une abstraction à coût zéro qui vérifie statiquement la sécurité des threads à la compilation tout en maintenant les caractéristiques de performance et l'ergonomie de Swift.
Le compilateur de Swift exige la conformité à Sendable pour tous les types passés à travers les frontières des Actors, utilisant une analyse statique pour vérifier la sécurité sans frais généraux à l'exécution. Les types de valeur, tels que struct et enum, sont implicitement Sendable car ils présentent des sémantiques de valeur et utilisent des optimisations de copie sur écriture pour prévenir l'état mutable partagé. Pour les types de référence (class), le compilateur exige une conformité explicite à Sendable, imposant que la classe soit final et contienne uniquement des propriétés Sendable, garantissant ainsi un état immutable ou synchronisé de manière interne qui ne peut pas être corrompu par un accès concurrent.
// Struct Sendable implicitement struct UserData: Sendable { let id: UUID let score: Int } // Classe finale Sendable explicitement avec état immutable final class Configuration: Sendable { let apiEndpoint: String let timeout: Duration init(endpoint: String, timeout: Duration) { self.apiEndpoint = endpoint self.timeout = timeout } } actor DataProcessor { func process(_ data: UserData) async { // Sûr : UserData est Sendable print("Processing \(data.id)") } }
Lors de l'architecture d'une application de trading financier en temps réel, notre équipe a mis en œuvre un PriceFeedActor responsable de l'agrégation des données de marché à partir de plusieurs connexions WebSocket, qui devait recevoir des charges utiles JSON analysées à partir d'un NetworkManager fonctionnant sur un thread d'arrière-plan. Au départ, nous avons utilisé une classe de type référence MarketData pour éviter de copier de grands ensembles de données lors de mises à jour à haute fréquence, mais le compilateur Swift nous a empêchés de passer ces objets directement à l'Actor car ils manquaient de conformité à Sendable et contenaient des dictionnaires mutables pour le caching des calculs. Cela nous a forcés à repenser notre modèle de données pour maintenir les garanties d'isolation de l'Actor sans sacrifier le débit requis pour les décisions de trading sub-millisecondes.
Nous avons refactorisé MarketData en un struct contenant un stockage privé pour les grands buffers d'octets et utilisé les mécanismes de copie sur écriture de Swift via ManagedBuffer pour partager le stockage sous-jacent jusqu'à ce qu'une mutation se produise. Cette approche a fourni une conformité Sendable implicite automatiquement, garantissant la sécurité à la compilation tout en minimisant la duplication de mémoire lors des opérations à forte lecture. Cependant, la complexité de la mise en œuvre de la logique de copie sur écriture manuelle a introduit une surcharge de maintenance, et nous risquions une dégradation des performances si le comportement de copie automatique était déclenché de manière inattendue lors des opérations d'écriture sur le chemin critique.
Nous avons conservé le type de référence MarketData mais l'avons restructuré en tant que final class avec uniquement des constantes let et des propriétés Sendable profondément immuables, ce qui nous a permis de partager une seule instance en lecture seule entre plusieurs Actors sans courses de données. Cela a préservé l'efficacité des sémantiques de référence pour de grands ensembles de données et éliminé entièrement la surcharge de copie, mais a nécessité une restructuration de notre stratégie de caching pour utiliser un état mutable isolé par Actor plutôt que des mutations internes de classe. Le changement architectural a exigé un refactoring significatif de notre couche de caching pour déplacer l'état mutable vers des Actors dédiés, augmentant la complexité du code mais garantissant des garanties strictes d'isolation.
En tant que mesure temporaire pour les classes bridées héritées d'Objective-C qui ne pouvaient pas être immédiatement refactorisées, nous les avons marquées avec @unchecked Sendable pour supprimer les avertissements du compilateur tout en vérifiant manuellement la sécurité des threads par le biais de verrous internes. Cela a permis une migration rapide vers le nouveau modèle Actor, mais a effectivement désactivé les garanties statiques de Swift et a réintroduit le risque de courses de données à l'exécution si notre logique de synchronisation manuelle contenait des erreurs. Par conséquent, nous avons restreint cette approche aux infrastructures de logging non critiques uniquement, évitant son utilisation pour des données financières en production où la sécurité était primordiale.
Nous avons adopté l'approche struct pour les données de streaming à haute fréquence utilisant des conceptions optimisées avec copie sur écriture, tout en réservant l'approche immuable class pour les objets de configuration statique accessibles par plusieurs Actors simultanément. Cette approche hybride a éliminé tous les plantages dus aux courses de données détectés lors des tests de stress, réduisant nos rapports de bogues liés à la concurrence de 94 % par rapport à l'architecture précédente basée sur GCD. Les vérifications Sendable à la compilation ont détecté trois conditions de course potentielles durant le développement qui auraient causé des plantages intermittents en production dans le précédent système de verrouillage manuel.
Pourquoi un type conformant à Sendable échoue-t-il toujours à compiler lorsqu'il est capturé par une closure transmise à une tâche asynchrone, et comment l'attribut @Sendable sur les closures résout-il cette ambiguïté ?
Bien qu'un type puisse être Sendable, les closures dans Swift capturent par référence par défaut, ce qui pourrait permettre des mutations ultérieures de la variable capturée après que la closure a été envoyée à un autre Actor. L'attribut de closure @Sendable restreint les captures aux valeurs Sendable et impose que la closure elle-même ne sorte pas du domaine concurrent de manière non sécurisée. Cela garantit que la closure et tout son état capturé maintiennent des garanties d'isolation à travers les frontières des Actors, prévenant l'introduction de courses de données par des listes de capture mutables dans des opérations asynchrones.
Comment le strict contrôle de la concurrence de Swift 6 affecte-t-il les en-têtes Objective-C importés implicitement, et quels mécanismes permettent la continuité de l'interopérabilité avec les anciens frameworks manquant d'annotations Sendable ?
Swift 6 introduit un contrôle de concurrence strict qui considère la plupart des types Objective-C comme non Sendable par défaut en raison de leur incapacité à fournir des garanties de sécurité statiques. Les développeurs doivent utiliser des déclarations d'importation @preconcurrency pour adopter progressivement des vérifications de sécurité ou annoter manuellement les en-têtes Objective-C avec les macros SWIFT_SENDABLE. Ces annotations permettent au compilateur de distinguer entre les objets hérités sûrs pour les threads et ceux nécessitant des frontières d'isolation, permettant l'interopérabilité sans compromettre la sécurité du code pur Swift.
Quelle est la différence fondamentale entre les méthodes non isolées au sein d'un Actor et les types Sendable, et quand l'appel d'une méthode non isolée sur une instance de classe mutable introduit-il un comportement indéfini ?
Les méthodes non isolées permettent un accès synchrone aux données d'un Actor depuis l'extérieur de son contexte d'isolation, mais elles s'exécutent sur l'exécuteur de l'appelant plutôt que sur l'exécuteur sériel de l'Actor. Cela exige que la méthode n'accède pas directement à l'état mutable de l'Actor, car cela contournerait les garanties d'isolation de l'Actor. Lorsqu'elles sont appliquées à un type de référence mutable qui n'est pas Sendable, les méthodes non isolées peuvent introduire des conditions de course si elles accèdent à un état mutable partagé sans synchronisation appropriée, entraînant une corruption de la mémoire ou un comportement indéfini.