Réponse à la question
Le modèle de concurrence de Swift a subi un durcissement significatif dans la version 6.0, introduisant des exigences d'isolation des données strictes qui s'étendent à travers les frontières des modules. Lorsque un module compilé avec une vérification de concurrence stricte appelle un module hérité marqué par @preconcurrency, le compilateur ne peut pas se fier uniquement à l'analyse statique pour garantir la sécurité, car l'implémentation de la fonction appelée pourrait précédé les garanties d'isolation des acteurs. Pour combler cette lacune, Swift intègre des exigences d'isolation en tant que métadonnées au sein des informations de type de la fonction et des tables de témoins, préservant la stabilité de l'ABI en ne modifiant pas la convention d'appel ou le mappage des symboles. À l'exécution, le code généré effectue une vérification dynamique en utilisant l'intrinsèque swift_task_isCurrentExecutor pour vérifier que la tâche courante s'exécute sur l'exécuteur sériel de l'acteur global requis avant de continuer ; si la vérification échoue, la tâche est mise en file d'attente de manière asynchrone sur le bon exécuteur ou un crash de diagnostic est déclenché, en fonction de la configuration de build.
Situation de la vie réelle
Une équipe de technologie financière maintenait un SDK d'analytique hérité (Module B) écrit en Swift 5.9 qui effectuait des calculs statistiques lourds sur des threads en arrière-plan, mais affichait parfois des mises à jour de l'interface utilisateur via des gestionnaires d'achèvement. En adoptant Swift 6 dans leur nouvelle application bancaire de consommation (Module A), ils devaient garantir que toutes les mises à jour de l'interface utilisateur se produisaient sur le MainActor sans réécrire immédiatement l'ensemble du SDK. Ils ont envisagé trois approches pour résoudre le problème de la frontière d'isolation.
La première option était une réécriture synchrone du SDK pour adopter les acteurs et types Sendable de Swift 6 dans toute sa structure. Bien que cela offrirait une sécurité à la compilation et aucun surcoût d'exécution, le coût d'ingénierie était prohibitif - estimé à trois mois - et introduisait un risque élevé de régression dans la logique de calcul critique. La seconde option consistait à envelopper manuellement chaque rappel de SDK dans DispatchQueue.main.async aux points d'appel dans le Module A. Cette approche était explicite et ne nécessitait aucun changement dans le SDK, mais produisait un code standard fragmenté et fragile, facile à manquer, ce qui pouvait entraîner des races de données lorsque de nouveaux développeurs ajoutaient des fonctionnalités. La troisième option utilisait des annotations @preconcurrency sur l'interface publique du SDK combinées avec les exigences d'isolation du MainActor.
L'équipe a choisi la troisième solution, annotant les rappels hérités avec @preconcurrency @MainActor. Cela a permis au Module A d'appeler ces méthodes avec l'assurance que le runtime de Swift vérifierait dynamiquement le contexte de l'exécuteur pendant la période de transition. Lorsque des violations se produisaient - par exemple lorsqu'un thread en arrière-plan tentait d'invoquer un rappel d'interface utilisateur - l'application plantait immédiatement dans les builds de débogage avec des diagnostics clairs, permettant aux développeurs d'identifier et de corriger progressivement les hypothèses liées au threading. Une fois que le SDK a été entièrement migré vers une concurrence stricte, ils ont supprimé @preconcurrency pour imposer exclusivement l'isolation statique, résultant en un code sans vérifications d'isolation à l'exécution et garantissant la sécurité des threads.
Ce que les candidats oublient souvent
Comment @preconcurrency affecte-t-il le nom de symbole manglé d'une fonction dans l'ABI, et pourquoi cela est-il important pour le lien dynamique ?
@preconcurrency ne modifie pas le nom de symbole manglé ou la convention d'appel de bas niveau d'une fonction, car les exigences d'isolation sont encodées dans les métadonnées de type et les tables de témoins plutôt que dans le symbole lui-même. Ce design est crucial pour la stabilité de l'ABI, car il permet aux auteurs de bibliothèques d'ajouter une isolation d'acteur aux API publiques existantes sans compromettre la compatibilité binaire avec les clients compilés précédemment. Les vérifications dynamiques sont injectées au point d'appel ou au point d'entrée par le compilateur en fonction des métadonnées, garantissant que les anciens binaires peuvent se lier sans problème avec de nouvelles bibliothèques conscientes de l'isolation.
Quelle est la différence entre une instance shared d'un acteur global déclarée comme let par rapport à var, et comment cela impacte-t-il l'unicité de l'exécuteur ?
Le protocole GlobalActor exige une propriété statique shared qui renvoie l'instance d'acteur sous-jacente, et cette propriété doit être déclarée comme une constante let pour garantir un exécuteur sériel unique au niveau du processus. Si shared était un var, l'exécuteur pourrait théoriquement être échangé à l'exécution, ce qui violerait l'invariant fondamental qu'un acteur global fournit une seule file d'attente sérielle pour toutes les opérations isolées, ce qui pourrait entraîner des races de données et briser les frontières d'isolation. Le compilateur Swift impose cela en exigeant que shared soit une propriété statique immuable, garantissant que swift_task_isCurrentExecutor compare toujours avec un objet exécuteur singleton cohérent.
Lorsqu'une fonction est isolée à un acteur global, pourquoi le compilateur émet-il parfois un saut vers l'exécuteur même lorsqu'elle est appelée depuis le même acteur, et comment le modificateur de paramètre isolated optimise-t-il cela ?
Le compilateur émet un saut vers l'exécuteur - ou au moins une vérification à l'exécution - lorsqu'il ne peut pas prouver statiquement que l'appelant s'exécute déjà sur l'exécuteur de l'acteur global cible, ce qui se produit généralement à travers les frontières de module ou lors d'appels via des types existents où les informations d'isolation sont effacées. Cette approche conservatrice assure la sécurité mais entraîne un surcoût de synchronisation. Les développeurs peuvent optimiser cela en utilisant le modificateur de paramètre isolated (par exemple, func process(isolation: isolated MainActor = #isolation)), qui passe explicitement le contexte d'isolation de l'appelant en tant qu'argument ; cela permet au compilateur d'omettre la vérification à l'exécution et le saut lorsque l'appelant prouve qu'il réside sur le même exécuteur, réduisant l'appel à une invocation directe de la fonction sans coût de changement de contexte.