Le modèle de l’acteur: Pourquoi erlang est-il spécial? Ou pourquoi avez-vous besoin d’une autre langue?

J’ai cherché à apprendre erlang et, en conséquence, j’ai lu (okay, skimming) le modèle de l’acteur.

D’après ce que je comprends, le modèle d’acteur est simplement un ensemble de fonctions (exécutées dans des threads légers appelés «processus» dans erlang), qui communiquent entre elles uniquement via le passage de messages.

Cela semble assez sortingvial à implémenter en C ++ ou dans tout autre langage:

class BaseActor { std::queue messages; CriticalSection messagecs; BaseMessage* Pop(); public: void Push(BaseMessage* message) { auto scopedlock = messagecs.AquireScopedLock(); messagecs.push(message); } virtual void ActorFn() = 0; virtual ~BaseActor() {} = 0; } 

Chacun de vos processus étant une instance d’un BaseActor dérivé. Les acteurs ne communiquent entre eux que par transmission de messages. (à savoir, pousser). Les acteurs s’enregistrent avec une carte centrale lors de l’initialisation qui permet aux autres acteurs de les trouver et permet à une fonction centrale de les parcourir.

Maintenant, je comprends que je manque, ou plutôt que je passe sous silence un problème important, à savoir: le manque de rendement signifie qu’un seul acteur peut injustement consumr un temps excessif. Mais les coroutines multi plates-formes sont-elles la première chose qui rend cela difficile en C ++? (Windows par exemple a des fibres.)

Y a-t-il autre chose qui me manque, ou le modèle est-il vraiment aussi évident?

Je n’essaie certainement pas de déclencher une guerre des flammes ici, je veux juste comprendre ce qui me manque, car c’est essentiellement ce que je fais déjà pour être capable de raisonner un peu sur le code concurrent.

Le code C ++ ne traite pas de l’équité, de l’isolement, de la détection de fautes ou de la dissortingbution, autant d’éléments que Erlang apporte dans le cadre de son modèle d’acteur.

  • Aucun acteur n’est autorisé à affamer un autre acteur (équité)
  • Si un acteur plante, cela ne devrait affecter que cet acteur (isolement)
  • Si un acteur plante, d’autres acteurs doivent être en mesure de détecter et de réagir à cette panne (détection de panne)
  • Les acteurs devraient pouvoir communiquer sur un réseau comme s’ils étaient sur la même machine (dissortingbution)

De plus, l’émulateur SMP du faisceau apporte la programmation JIT des acteurs, les déplaçant vers le kernel qui est actuellement celui qui est le moins utilisé et qui veille également sur les threads sur certains cœurs s’ils ne sont plus nécessaires.

De plus, toutes les bibliothèques et tous les outils écrits dans Erlang peuvent supposer que c’est ainsi que le monde fonctionne et qu’il doit être conçu en conséquence.

Ces choses ne sont pas impossibles à faire en C ++, mais elles deviennent de plus en plus difficiles si vous ajoutez le fait que Erlang fonctionne sur presque toutes les configurations hw et os principales.

edit: Je viens de trouver une description par Ulf Wiger de ce qu’il considère être la concurrence de style erlang.

Je n’aime pas me citer, mais d’ après la première règle de programmation de Virding

Tout programme concurrent suffisamment compliqué dans une autre langue contient une implémentation ad hoc ralentie par bogue spécifiée de manière informelle de la moitié de Erlang.

En ce qui concerne Greenspun. Joe (Armstrong) a une règle similaire.

Le problème n’est pas de mettre en place des acteurs, ce n’est pas si difficile. Le problème est de tout faire fonctionner ensemble: processus, communication, ramasse-miettes, primitives de langue, gestion des erreurs, etc. Par exemple, l’utilisation de threads de système d’exploitation évolue mal. Ce serait comme essayer de “vendre” un langage OO où vous ne pouvez avoir que 1 k objects et ils sont lourds à créer et à utiliser. De notre sharepoint vue, la simultanéité est l’abstraction de base pour structurer les applications.

Se laisser emporter alors je vais m’arrêter ici.

C’est en fait une excellente question et a reçu d’excellentes réponses qui ne sont peut-être pas encore convaincantes.

Pour append de la nuance et mettre l’accent sur les autres bonnes réponses, considérez ce qu’est Erlang (par rapport aux langages classiques tels que C / C ++) afin d’obtenir une tolérance aux pannes et une disponibilité.

Tout d’abord, il enlève les serrures. Le livre de Joe Armstrong expose cette expérience de pensée: supposez que votre processus acquiert un verrou et se bloque immédiatement (un problème de mémoire provoque le blocage du processus ou une panne du système). La prochaine fois qu’un processus attend ce même verrou, le système vient de se bloquer. Cela pourrait être un verrou évident, comme dans l’appel AquireScopedLock () dans l’exemple de code; ou il peut s’agir d’un verrou implicite acquis pour vous par un gestionnaire de mémoire, par exemple lors de l’appel de malloc () ou de free ().

Dans tous les cas, votre plantage de processus a maintenant empêché tout le système de progresser. Fini. Fin de l’histoire. Votre système est mort À moins que vous ne puissiez garantir que chaque bibliothèque que vous utilisez en C / C ++ n’appelle jamais malloc et n’acquiert jamais un verrou, votre système n’est pas tolérant aux pannes. Les systèmes Erlang peuvent tuer des processus à volonté lorsqu’ils sont soumis à une charge importante, afin de progresser, de sorte qu’à l’échelle de vos processus Erlang doivent être éliminables (à n’importe quel point d’exécution) afin de maintenir le débit.

Il existe une solution de contournement partielle: utiliser des baux partout au lieu de verrous, mais vous ne pouvez pas garantir que toutes les bibliothèques que vous utilisez le font également. Et la logique et le raisonnement à propos de l’exactitude deviennent vraiment très velus. De plus, les baux se rétablissent lentement (à l’expiration du délai d’expiration), de sorte que l’ensemble de votre système est très lent face aux pannes.

Deuxièmement, Erlang élimine le typage statique, ce qui permet l’échange de code à chaud et l’exécution simultanée de deux versions du même code. Cela signifie que vous pouvez mettre à niveau votre code à l’exécution sans arrêter le système. C’est ainsi que les systèmes restnt en veille pendant neuf ou trente-deux ms. Ils sont simplement mis à niveau. Vos fonctions C ++ devront être manuellement reliées pour être mises à niveau et l’exécution de deux versions simultanément n’est pas prise en charge. Les mises à niveau de code nécessitent une indisponibilité du système, et si vous avez un grand cluster qui ne peut pas exécuter plusieurs versions de code à la fois, vous devez arrêter le cluster en entier en une fois. Aie. Et dans le monde des télécommunications, pas tolérable.

En outre, Erlang enlève la mémoire partagée et partage le nettoyage de la mémoire partagée; chaque processus léger est collecté indépendamment. Ceci est une simple extension du premier point, mais souligne que pour une vraie tolérance aux pannes, vous avez besoin de processus qui ne sont pas interreliés en termes de dépendances. Cela signifie que vos pauses GC par rapport à Java sont tolérables (petites au lieu de faire une pause d’une demi-heure pour un GC complet) pour les gros systèmes.

Il existe des bibliothèques d’acteurs pour C ++:

Et une liste de certaines bibliothèques pour d’autres langues.

Il s’agit beaucoup moins du modèle de l’acteur et beaucoup plus sur la difficulté d’écrire correctement quelque chose d’analogue à OTP en C ++. De plus, différents systèmes d’exploitation fournissent des outils de débogage et de système radicalement différents, et la VM d’Erlang et plusieurs constructions de langage prennent en charge une manière uniforme de déterminer exactement ce que tous ces processus seraient difficiles à faire de manière uniforme. du tout) sur plusieurs plates-formes. (Il est important de rappeler qu’Erlang / OTP est antérieur au buzz actuel sur le terme «modèle acteur», donc dans certains cas, ce genre de discussions compare des pommes et des ptérodactyles; les grandes idées sont sujettes à une invention indépendante.)

Tout cela signifie que si vous pouvez certainement écrire une suite de programmes “d’acteurs” dans une autre langue (je sais, je l’ai fait depuis longtemps en Python, C et Guile sans m’en rendre compte avant de rencontrer Erlang, y compris une forme de moniteurs et liens, et avant que j’aie jamais entendu le terme «modèle acteur», il est extrêmement difficile de comprendre comment les processus que votre code génère et ce qui se passe entre eux . Erlang applique des règles qu’un système d’exploitation ne peut tout simplement pas faire sans une révision majeure du kernel – des modifications majeures du kernel qui ne seraient probablement pas bénéfiques globalement. Ces règles se manifestent à la fois par des ressortingctions générales sur le programmeur (qui peuvent toujours être trouvées si vous en avez réellement besoin) et des promesses de base garantissant le système pour le programmeur (qui peut être délibérément cassé si vous en avez vraiment besoin).

Par exemple, il impose que deux processus ne puissent pas partager l’état pour vous protéger des effets secondaires. Cela ne signifie pas que chaque fonction doit être “pure” dans le sens où tout est référentiellement transparent (évidemment pas, même si la plupart des projets Erlang font clairement référence à la transparence de votre programme). les processus ne créent pas constamment des conditions de course liées à un état ou à un conflit partagé. (C’est d’ailleurs ce que signifie “effets secondaires” dans le contexte d’Erlang, sachant que cela peut vous aider à déchiffrer une partie de la discussion en vous demandant si Erlang est “vraiment fonctionnel ou non” par rapport aux langages Haskell ou toy “pure” .)

D’autre part, le runtime Erlang garantit la livraison des messages. C’est quelque chose qui manque cruellement dans un environnement où vous devez communiquer uniquement sur des ports non gérés, des canaux, de la mémoire partagée et des fichiers communs que le kernel du système d’exploitation est le seul à gérer. runtime fournit). Cela ne signifie pas que Erlang garantit le RPC (de toute façon, le passage des messages n’est pas RPC, ni l’invocation de la méthode!), Il ne garantit pas que votre message est correctement adressé et qu’il ne vous promet pas essayer d’envoyer un message à existe ou est vivant non plus. Cela garantit simplement la livraison si la chose à laquelle votre envoi arrive est valide à ce moment.

Cette promesse repose sur la promesse que les moniteurs et les liens sont exacts. Et sur la base de ce que le runtime Erlang rend le concept entier de “cluster réseau” en quelque sorte disparaître une fois que vous saisissez ce qui se passe avec le système (et comment utiliser erl_connect …). Cela vous permet de passer par-dessus un ensemble de cas difficiles d’access simultané, ce qui donne une grande longueur d’avance au codage de l’affaire réussie au lieu de vous perdre dans le marécage des techniques défensives requirejses pour une programmation simultanée nue.

Il ne s’agit donc pas vraiment d’ avoir besoin d’ Erlang, le langage, le runtime et le protocole OTP déjà existant, d’être exprimé de manière assez claire, et d’implémenter quelque chose de proche dans une autre langue étant extrêmement difficile. OTP est juste un acte difficile à suivre. Dans le même ordre d’idées, nous n’avons pas vraiment besoin de C ++, nous pourrions simplement coller à l’entrée binary brute, Brainfuck et considérer Assembler comme un langage de haut niveau. Nous n’avons pas non plus besoin de trains ou de navires, car nous soaps tous comment marcher et nager.

Cela dit, le bytecode de la machine virtuelle est bien documenté et un certain nombre de langages alternatifs ont été compilés ou fonctionnent avec l’environnement d’exécution d’Erlang. Si nous divisons la question en une partie langage / syntaxe (“Dois-je comprendre les Moon Runes pour faire de la concurrence?”) Et une partie plate-forme (“OTP est-il le moyen le plus mature de faire de la concurrence? , les pièges les plus courants à trouver dans un environnement concurrent et dissortingbué? “) alors la réponse est (” non “,” oui “).

Casablanca est un autre nouveau-né du bloc acteur. Une acceptation asynchrone typique ressemble à ceci:

 PID replyTo; NameQuery request; accept_request().then([=](std::tuple request) { if (std::get<0>(request) == FirstName) std::get<1>(request).send("Niklas"); else std::get<1>(request).send("Gustafsson"); } 

(Personnellement, je trouve que CAF fait un meilleur travail pour cacher la correspondance de motif derrière une interface agréable.)