Back home

Swift Concurrency Series 05|La différence entre Actor et les méthodes d'écriture traditionnelles thread-safe

Les acteurs ne sont pas des « verrous rapides ». Ce qu’ils changent réellement, c’est la façon dont l’État partagé est organisé.

Lorsque vous entendrez Actor pour la première fois, vous le comprendrez inconsciemment comme « un outil de sécurité des threads fourni par Swift ».

Cette compréhension n’est pas complètement fausse, mais si vous vous arrêtez ici, il sera facile d’écrire du code qui “utilise des acteurs, mais la structure est toujours désordonnée”.

Parce que la chose la plus importante à propos de Actor est qu’il change l’idée par défaut :

L’état mutable partagé ne doit pas d’abord être exposé à tout le monde, puis trouver des moyens de le protéger ; une approche plus raisonnable consiste à l’isoler d’abord, puis à décider comment y accéder depuis l’extérieur.

Cela peut ressembler à une différence de formulation, mais il s’agit en réalité d’un tournant dans les habitudes de conception de la concurrence.

1. La chose la plus dangereuse en matière de concurrence est souvent l’état mutable partagé

La plupart des bugs de concurrence commencent à partir de :

  • Deux tâches lisent et écrivent le même cache en même temps
  • Une tâche n’a pas encore été écrite et une autre tâche a déjà porté un jugement basé sur l’ancienne valeur.
  • Plusieurs pages modifient un statut global en même temps
  • Un certain service est responsable du rafraîchissement des jetons, de la consommation des jetons et de la diffusion des résultats.

Ces problèmes peuvent prendre de nombreuses formes, mais les causes profondes sont souvent les mêmes : ** L’état mutable partagé n’a pas de limites claires. **

Une fois que les frontières des États ne sont pas claires, tout changement dans l’ordre de planification des tâches peut amplifier les bugs.

2. Le problème avec les solutions traditionnelles de sécurité des threads est qu’elles ne sont pas suffisamment centralisées.

Les solutions que nous avons couramment utilisées dans le passé incluent :

  • NSLock
  • file d’attente série
  • verrouillage en lecture-écriture
  • Accord d’équipe selon lequel “cet objet n’est accessible que sur une certaine file d’attente”

Aucune de ces solutions n’est fausse et de nombreux systèmes matures sont encore utilisés de manière stable aujourd’hui. Leur vrai problème est :

**Les règles d’accès sont facilement dispersées. **

Au début, vous vous souviendrez peut-être encore :

  • Ce cache doit être accessible avec un verrou
  • Ce dictionnaire ne peut être modifié que dans la file d’attente série
  • Un certain état ne peut être lu et écrit que dans le thread principal

Mais à mesure que le nombre de points d’appel augmente, ces règles vont peu à peu se déformer. Le plus ennuyeux avec les bugs de concurrence est qu’il n’est souvent plus possible de garantir que toute l’équipe respecte strictement ces règles dispersées.

3. L’idée centrale d’Actor est de « réduire d’abord le partage »

C’est le point le plus important qui mérite d’être répété.

L’idée traditionnelle ressemble davantage à :

  1. Avoir d’abord un statut partagé
  2. Pensez ensuite aux moyens de le protéger via des verrous, des files d’attente et des conventions

L’idée de l’acteur est plus proche :

  1. Cet état ne doit pas être accessible arbitrairement.
  2. Je l’ai d’abord mis dans la limite d’isolement
  3. Le monde extérieur ne peut interagir avec lui que via l’interface contrôlée

Le plus grand changement que cela apporte est que nous sommes obligés de réfléchir à la propriété du statut, au lieu d’accepter que tout le monde puisse y toucher directement.

Par exemple, si un coordinateur d’actualisation de jeton est simplement un objet global doté d’un ensemble de verrous, la mentalité par défaut est « Tout le monde utilise cet état et je dois le protéger ». S’il s’agit de Actor, la mentalité par défaut deviendra “Cet état est géré par lui, et les autres ne peuvent lui demander que des résultats ou envoyer des commandes”.

C’est ainsi que la conception change.

4. Cette approche d’isolement est plus adaptée aux projets complexes

Car ce que craignent le plus les projets complexes, c’est la prolifération des règles.

Une fois l’accès à l’État bloqué par Actor, de nombreux problèmes seront exposés plus tôt :

-Qui contrôle ces données ?

  • L’exigence externe est-elle un instantané, une valeur dérivée ou une opération impérative ?
  • Quelles opérations doivent avoir une sémantique sérielle et lesquelles ne sont que des requêtes en lecture seule

Ces problèmes étaient souvent repoussés dans l’ancien modèle, car tout le monde par défaut “partageait d’abord, puis verrouillait s’il y avait des problèmes”. Les acteurs, en revanche, imposent la conception des limites plus tôt.

Par conséquent, je préfère considérer Actor comme un outil de conception de concurrence plutôt que comme un simple outil de sécurité des threads.

5. Quel est le meilleur endroit pour placer l’acteur ?

Tous les objets ne méritent pas d’être transformés en acteurs. Il est plus adapté aux rôles qui répondent à la fois aux caractéristiques suivantes :

  • Avoir partagé un état mutable
  • Sera accessible simultanément par plusieurs tâches
  • La cohérence est importante
  • Les méthodes d’accès doivent être coordonnées de manière centralisée

Les exemples typiques incluent :

  • Coordinateur du cache d’images
  • Coordinateur du rafraîchissement des jetons
  • Télécharger le centre d’enregistrement des tâches
  • Une sorte d’accesseur de ressources global
  • Centre de messages qui nécessite une consommation en série d’événements

Ce que ces objets ont en commun, c’est qu’ils sont des centres d’état partagés entre les tâches et sujets à des problèmes de concurrence.

S’il s’agit simplement d’un service de fonction pur ou d’un objet d’état qui n’est utilisé que brièvement dans une seule page, un acteur n’est peut-être pas nécessaire.

6. Quelle est la différence essentielle entre Actor et “Ajouter un verrou à toute la classe” ?

En apparence, ils peuvent tous effectuer « plusieurs tâches sans changer de statut ». Mais l’expérience en ingénierie varie considérablement.

Lorsque vous verrouillez une classe entière, les questions courantes sont :

  • L’appelant peut toujours obtenir de nombreux détails internes
  • La granularité des verrous peut facilement devenir incontrôlable
  • Certaines méthodes font appel à d’autres ressources de synchronisation, formant des imbrications complexes.
  • Les membres de l’équipe peuvent toujours contourner les règles et accéder directement aux statuts auxquels ils ne devraient pas accéder

Acteur, au moins au niveau sémantique, exprime clairement :

  • Ce statut n’est pas librement partagé
  • L’accès à l’isolation croisée doit explicitement traverser la limite asynchrone
  • Accéder à cet état est en soi un comportement contraint

En d’autres termes, les acteurs non seulement “aident à ajouter une protection”, mais rendent également les méthodes d’accès non sécurisées plus difficiles à écrire avec désinvolture.

7. L’acteur ne peut rien résoudre

Cela doit être clair. Lorsque vous apprendrez ce contenu pour la première fois, vous aurez l’illusion que la sécurité des threads lui sera laissée à l’avenir.

En fait, les acteurs ne résolvent qu’un seul type de problème : isoler l’état partagé.

Cela ne résout pas :

  • La séquence commerciale elle-même est-elle raisonnable ?
  • Les anciennes tâches doivent-elles continuer après avoir quitté la page ?
  • Si le résultat est expiré
  • La frontière de l’État est-elle mal tracée ?

Par exemple, si le cache des résultats de recherche est transformé en acteur, mais que la page permet toujours aux anciennes requêtes d’écraser les nouveaux résultats de mots clés, le bug existera toujours. Parce que le problème ici est que la « validité des résultats » n’est pas modélisée en premier lieu.

Les acteurs sont donc importants, mais ils ne constituent pas une panacée pour la concurrence.

8. Les deux directions d’acteur les plus facilement utilisées à mauvais escient

1. Essayez de tout intégrer dans l’Actor

Une fois que les acteurs seront considérés comme « plus sûrs tant qu’ils sont utilisés », ils commenceront à suremballer :

  • L’état qui n’est évidemment utilisé que dans un contexte mono-thread est également packagé en tant qu’Acteur
  • La logique qui est évidemment plus adaptée au traitement de fonctions pures est également forcée dans Actor
  • Au final, l’ensemble du système est rempli de frontières d’accès asynchrones, et la structure devient plus lourde.

Plus d’acteurs n’est pas toujours mieux. La clé est de les placer là où l’État partagé est réellement nécessaire pour être isolé.

2. Traitez les acteurs comme des « preuves de sécurité »

Une fois que des acteurs sont ajoutés à du code, l’équipe supposera que ce morceau de code est « thread-safe ». Mais de nombreux bugs proviennent de :

  • Mauvaise conception du cycle de vie
  • Mauvais flux d’état
  • Mauvaise relation entre les tâches

L’acteur peut garantir “Ne jouez pas avec la concurrence et ne touchez pas à mon état interne”, mais il ne peut pas garantir “le processus métier est correct”.

9. Une question de jugement plus pratique

Si vous hésitez à savoir si un objet doit être conçu comme un acteur, je vous suggère de ne pas demander d’abord « Est-ce que ce sera un thread dangereux », mais d’abord de demander :

  1. Contient-il un état mutable partagé important ?
  2. Cet état sera-t-il accessible par plusieurs tâches en même temps ?
  3. Est-ce que je souhaite que le monde extérieur interagisse avec lui uniquement via des interfaces contrôlées ?
  4. Si la limite d’isolement n’est pas ajoutée, sera-t-il facile pour l’équipe d’en abuser à l’avenir ?

Si la réponse à la plupart de ces questions est « oui », alors c’est probablement une bonne solution pour l’acteur.

10. Conclusion : La vraie valeur d’Actor est de changer l’état mutable partagé de l’exposition par défaut à l’isolement par défaut.

Pour le dire sous une forme plus courte, je dirais :

Actor Ce qui a vraiment changé, c’est que « l’état mutable partagé n’est finalement plus exposé à tout le monde par défaut ».

La pensée traditionnelle est plutôt la suivante : partager d’abord, puis protéger. L’idée d’Actor est la suivante : isoler d’abord, puis décider comment y accéder.

Dans les systèmes concurrents complexes, ce changement de pensée est souvent plus important qu’« un outil de sécurité des threads supplémentaire ».

FAQ

What to read next

Related

Continue reading