web-dev-qa-db-fra.com

Le modèle d'acteur: Pourquoi Erlang est-il spécial? Ou, pourquoi avez-vous besoin d'une autre langue pour cela?

J'ai cherché à apprendre l'erlang et, par conséquent, j'ai lu (d'accord, écrémé) sur le modèle de l'acteur.

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

Cela semble assez trivial à implémenter en C++, ou tout autre langage:

class BaseActor {
    std::queue<BaseMessage*> 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 à l'initialisation qui permet à d'autres acteurs de les retrouver et permet à une fonction centrale de les parcourir.

Maintenant, je comprends que je manque, ou plutôt que je passe sous silence une question importante ici, à savoir: le manque de rendement signifie qu'un seul acteur peut injustement consommer trop de temps. Mais les coroutines multiplateformes sont-elles la principale 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 si é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 pouvoir raisonner un peu sur le code concurrent.

76
Jonathan Winks

Le code C++ ne traite pas de l'équité, de l'isolement, de la détection ou de la distribution des défauts, qui sont toutes des choses qu'Erlang apporte dans le cadre de son modèle d'acteur.

  • Aucun acteur n'est autorisé à affamer un autre acteur (équité)
  • Si un acteur tombe en panne, cela ne devrait affecter que cet acteur (isolement)
  • Si un acteur tombe en panne, les autres acteurs devraient être capables de détecter et de réagir à ce crash (détection de faute)
  • Les acteurs doivent pouvoir communiquer sur un réseau comme s'ils étaient sur la même machine (distribution)

L'émulateur de faisceau SMP apporte également l'ordonnancement JIT des acteurs, en les déplaçant vers le noyau qui est à l'heure actuelle celui qui utilise le moins et hiberne également les threads sur certains cœurs s'ils ne sont plus nécessaires.

De plus, toutes les bibliothèques et les outils écrits en Erlang peuvent supposer que c'est ainsi que le monde fonctionne et être conçus 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 qu'Erlang fonctionne sur presque toutes les principales configurations de matériel et d'OS.

edit: Je viens de trouver une description par lf Wiger sur ce qu'il considère comme la concurrence de style erlang.

83
Lukas

Je n'aime pas me citer, mais de Première règle de programmation de Virding

Tout programme simultané suffisamment compliqué dans une autre langue contient une implémentation lente ad hoc spécifiée de manière informelle et bogue de la moitié d'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 faire en sorte que tout fonctionne ensemble: processus, communication, garbage collection, primitives de langage, gestion des erreurs, etc. Ce serait comme essayer de "vendre" un langage OO où vous ne pouvez avoir que 1k objets et ils sont lourds à créer et à utiliser. De notre point de vue, la concurrence est l'abstraction de base pour la structuration d'applications .

Je m'emballe donc je m'arrête ici.

29
rvirding

Il s'agit en fait d'une excellente question et a reçu d'excellentes réponses qui ne sont peut-être pas encore convaincantes.

Pour ajouter de l'ombre et de l'emphase aux autres bonnes réponses déjà ici, considérez ce que Erlang enlève (par rapport aux langages à usage général traditionnels tels que C/C++) afin d'atteindre la tolérance aux pannes et la disponibilité.

Tout d'abord, il enlève les verrous. Le livre de Joe Armstrong présente cette expérience de pensée: supposez que votre processus acquière un verrou, puis se bloque immédiatement (un problème de mémoire provoque le crash du processus ou la panne de courant d'une partie 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 en votre nom par un gestionnaire de mémoire, par exemple lors de l'appel à malloc () ou free ().

Dans tous les cas, le plantage de votre processus a désormais empêché l'ensemble du 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 de verrou, votre système n'est pas tolérant aux pannes. Les systèmes Erlang peuvent tuer les processus à volonté lorsqu'ils sont soumis à une forte charge afin de progresser, donc à grande échelle, vos processus Erlang doivent être éliminables (à tout moment de l'exécution) afin de maintenir le débit.

Il existe une solution partielle: utiliser des baux partout au lieu de verrous, mais vous n'avez aucune garantie que toutes les bibliothèques que vous utilisez le font également. Et la logique et le raisonnement sur l'exactitude deviennent vraiment poilus rapidement. De plus, les baux se rétablissent lentement (après l'expiration du délai d'expiration), de sorte que l'ensemble de votre système est devenu très lent en cas d'échec.

Deuxièmement, Erlang supprime la saisie statique, qui à son tour 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 lors de l'exécution sans arrêter le système. C'est ainsi que les systèmes restent en place pendant neuf 9 ou 32 ms de temps d'arrêt/an. Ils sont simplement mis à niveau sur place. Vos fonctions C++ devront être reconnectées manuellement pour être mises à niveau, et l'exécution de deux versions en même temps n'est pas prise en charge. Les mises à niveau de code nécessitent un temps d'arrêt du système, et si vous avez un grand cluster qui ne peut pas exécuter plus d'une version de code à la fois, vous devrez supprimer l'ensemble du cluster à la fois. Aie. Et dans le monde des télécommunications, pas tolérable.

De plus, Erlang supprime la mémoire partagée et la collecte de déchets partagée; chaque processus léger est collecté de manière indépendante. Il s'agit d'une simple extension du premier point, mais souligne que pour une véritable tolérance aux pannes, vous avez besoin de processus qui ne sont pas interdépendants en termes de dépendances. Cela signifie que votre GC s'arrête par rapport à Java sont tolérables (petit au lieu de faire une pause d'une demi-heure pour un GC de 8 Go)) pour les gros systèmes.

21
jaten

Il existe de véritables bibliothèques d'acteurs pour C++:

Et ne liste de certaines bibliothèques pour d'autres langues.

14
Alexey Romanov

Il s'agit beaucoup moins du modèle d'acteur et beaucoup plus de la difficulté d'écrire correctement quelque chose d'analogue à OTP en C++. En outre, différents systèmes d'exploitation offrent des outils de débogage et de système radicalement différents, et Erlang VM et plusieurs constructions de langage prennent en charge une manière uniforme de déterminer exactement ce que tous ces processus peuvent faire, ce qui serait très difficile à réaliser). faire de manière uniforme (ou peut-être du tout) sur plusieurs plates-formes. (Il est important de se rappeler que Erlang/OTP est antérieur au buzz actuel sur le terme "modèle d'acteur", donc dans certains cas, ce type de discussions compare des pommes et 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 "modèle d'acteur" dans une autre langue (je sais, je l'ai fait pendant 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 d'acteur"), comprendre comment les processus que votre code engendre et ce qui se passe parmi eux est extrêmement difficile. Erlang applique des règles qu'un OS ne peut tout simplement pas faire sans révisions majeures du noyau - des révisions du noyau qui ne seraient probablement pas avantageuses dans l'ensemble. Ces règles se manifestent à la fois comme des restrictions générales sur le programmeur (qui peuvent toujours être contournées si vous en avez vraiment besoin) et des promesses de base que le système garantit au programmeur (qui peuvent être délibérément brisées si vous en avez vraiment besoin).

Par exemple, il impose que deux processus ne peuvent pas partager l'état pour vous protéger contre les effets secondaires. Cela ne signifie pas que chaque fonction doit être "pure" dans le sens où tout est référentiellement transparent (évidemment pas, bien que rendre autant de votre programme référentiel transparent comme pratique est un objectif de conception clair de la plupart des Erlang projets), mais plutôt que deux processus ne créent pas constamment des conditions de concurrence liées à un état partagé ou à un conflit. (C'est plus ce que les "effets secondaires" signifient dans le contexte d'Erlang, soit dit en passant; 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 à Haskell ou aux langages "purs" de jouets) .)

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 noyau du système d'exploitation est le seul à gérer (et la gestion du noyau du système d'exploitation de ces ressources est nécessairement extrêmement minime par rapport à ce qu'Erlang runtime fournit). Cela ne signifie pas qu'Erlang garantit le RPC (de toute façon, le passage de message est pas RPC, ce n'est pas non plus une invocation de méthode!), Il ne promet pas que votre message est adressé correctement, et il ne le fait pas promettez qu'un processus auquel vous essayez d'envoyer un message existe ou existe. Il garantit simplement la livraison si la chose à laquelle vous envoyez est valide à ce moment-là.

Cette promesse repose sur la promesse que les moniteurs et les liens sont exacts. Et sur la base de cela, le runtime Erlang fait fondre le concept de "cluster réseau" une fois que vous comprenez ce qui se passe avec le système (et comment utiliser erl_connect ...). Cela vous permet de sauter déjà sur un ensemble de cas de concurrence délicats, ce qui donne une bonne longueur d'avance sur le codage du cas réussi au lieu de s'embourber dans le marécage de techniques défensives requises pour la programmation simultanée nue.

Il ne s'agit donc pas vraiment de besoin Erlang, le langage, il s'agit du runtime et de l'OTP déjà existant, s'exprimant de manière plutôt propre, et l'implémentation de tout ce qui lui est proche dans une autre langue est extrêmement difficile. OTP est juste un acte difficile à suivre. Dans la même veine, nous n'avons pas vraiment besoin C++, nous pourrions simplement nous en tenir à l'entrée binaire brute, Brainfuck et considérer Assembler comme notre langage de haut niveau. Nous n'avons pas non plus besoin de trains ou de navires, car nous savons tous comment marcher et nager.

Cela dit, le bytecode de la VM est bien documenté, et un certain nombre de langages alternatifs ont émergé qui le compilent ou fonctionnent avec le runtime Erlang. Si nous divisons la question en une partie langage/syntaxe ("Dois-je comprendre Moon Runes pour faire de la concurrence?") Et une partie plate-forme ("Est-ce que OTP est le moyen le plus mature de faire de la concurrence, et cela me guidera-t-il , les pièges les plus courants dans un environnement simultané et distribué? "), la réponse est (" non "," oui ").

3
zxq9

Casablanca est un autre nouveau venu sur le bloc de modèles d'acteurs. Une acceptation asynchrone typique ressemble à ceci:

PID replyTo;
NameQuery request;
accept_request().then([=](std::Tuple<NameQuery,PID> 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 le motif correspondant derrière une interface Nice.)

2
mavam