web-dev-qa-db-fra.com

Les acteurs de Scala sont-ils similaires aux coroutines de Go?

Si je voulais porter une bibliothèque Go qui utilise des Goroutines, est-ce que Scala serait un bon choix car sa structure de boîte de réception/akka est de nature similaire aux coroutines?

70
loyalflow

Non, ils ne le sont pas. Les goroutines sont basées sur la théorie de la communication des processus séquentiels, telle que spécifiée par Tony Hoare en 1978. L'idée est qu'il peut y avoir deux processus ou threads qui agissent indépendamment l'un de l'autre mais partagent un "canal", lequel processus/thread met les données dans et l'autre processus/thread consomme. Les implémentations les plus importantes que vous trouverez sont les canaux de Go et core.async De Clojure, mais à l'heure actuelle, ils sont limités à l'exécution actuelle et ne peuvent pas être distribués, même entre deux exécutions sur la même boîte physique.

Le CSP a évolué pour inclure une algèbre de processus formelle statique pour prouver l'existence de blocages dans le code. C'est une fonctionnalité vraiment sympa, mais ni Goroutines ni core.async Ne la supportent actuellement. Si et quand ils le font, il sera extrêmement agréable de savoir avant d'exécuter votre code si un blocage est possible ou non. Cependant, CSP ne prend pas en charge la tolérance aux pannes de manière significative, donc vous, en tant que développeur, devez comprendre comment gérer les défaillances qui peuvent se produire des deux côtés des canaux, et une telle logique finit par se répandre partout dans l'application.

Les acteurs, comme spécifié par Carl Hewitt en 1973, impliquent des entités qui ont leur propre boîte aux lettres. Ils sont asynchrones par nature et ont une transparence de localisation qui couvre les exécutions et les machines - si vous avez une référence (Akka) ou PID (Erlang) d'un acteur, vous pouvez le transmettre par message. C'est également là que certaines personnes trouvent des défauts dans les implémentations basées sur les acteurs, dans la mesure où vous devez avoir une référence à l'autre acteur afin de lui envoyer un message, couplant ainsi directement l'expéditeur et le destinataire. Dans le modèle CSP, le canal est partagé et peut être partagé par plusieurs producteurs et consommateurs. D'après mon expérience, ce n'est pas vraiment un problème. J'aime l'idée de références proxy qui signifie que mon code n'est pas jonché de détails d'implémentation sur la façon d'envoyer le message - j'en envoie juste un, et où que se trouve l'acteur, il le reçoit. Si ce nœud tombe en panne et que l'acteur se réincarne ailleurs, c'est théoriquement transparent pour moi.

Les acteurs ont une autre fonctionnalité très agréable - la tolérance aux pannes. En organisant les acteurs dans une hiérarchie de supervision selon la spécification OTP conçue dans Erlang, vous pouvez créer un domaine d'échec dans votre application. Tout comme les classes de valeur/DTO/tout ce que vous voulez appeler, vous pouvez modéliser l'échec, comment il doit être traité et à quel niveau de la hiérarchie. C'est très puissant, car vous avez très peu de capacités de gestion des pannes à l'intérieur du CSP.

Les acteurs sont également un paradigme de concurrence, où l'acteur peut avoir un état mutable à l'intérieur de celui-ci et une garantie d'aucun accès multithread à l'état, à moins que le développeur qui construit un système basé sur l'acteur ne l'introduise accidentellement, par exemple en enregistrant l'acteur comme auditeur pour un rappel, ou devenir asynchrone à l'intérieur de l'acteur via Futures.

Plug sans vergogne - J'écris un nouveau livre avec le chef de l'équipe Akka, Roland Kuhn, intitulé Reactive Design Patterns où nous discutons de tout cela et plus encore. Fils verts, CSP, boucles d'événements, itérés, extensions réactives, acteurs, futures/promesses, etc. Attendez-vous à voir un MEAP sur Manning au début du mois prochain.

Bonne chance!

128
jamie

Il y a deux questions ici:

  • Scala est-il un bon choix pour porter goroutines?

C'est une question facile, car Scala est un langage à usage général, qui n'est ni pire ni meilleur que beaucoup d'autres que vous pouvez choisir de "porter des goroutines".

Il y a bien sûr de nombreuses opinions sur pourquoi Scala est meilleur ou pire comme langue (par exemple ici est le mien), mais ce ne sont que des opinions, et ne les laissez pas vous arrêter. Puisque Scala est à usage général, cela se résume "à peu près" à: tout ce que vous pouvez faire en langage X, vous pouvez le faire en Scala. Si cela semble trop large .. que diriez-vous suite en Java :)

  • Les acteurs Scala sont-ils similaires à goroutines?

La seule similitude (à part la sélection) est qu'ils ont tous deux à voir avec la concurrence et la transmission des messages. Mais c'est là où la similitude se termine.

Étant donné que la réponse de Jamie a donné un bon aperçu des acteurs Scala, je vais me concentrer davantage sur Goroutines/core.async, mais avec une intro de modèle d'acteur.

Les acteurs aident les choses à être "distribuées sans souci"


Lorsqu'une pièce "sans souci" est généralement associée à des termes tels que: fault tolerance, resiliency, availability, etc.

Sans entrer dans les détails de Grave sur le fonctionnement des acteurs, en deux termes simples, les acteurs ont à voir avec:

  • Localité : chaque acteur a une adresse/référence que les autres acteurs peuvent utiliser pour envoyer des messages
  • Comportement : une fonction qui est appliquée/appelée lorsque le message arrive à un acteur

Imaginez des "processus parlants" où chaque processus a une référence et une fonction qui est appelée lorsqu'un message arrive.

Bien sûr, il y a beaucoup plus (par exemple, consultez Erlang OTP , ou akka docs ), mais les deux ci-dessus sont un bon début.

Là où ça devient intéressant avec les acteurs, c'est la mise en œuvre. Erlang OTP et Scala AKKA. Pour l'instant, ils visent tous deux à résoudre la même chose, mais il y a quelques différences. Voyons-en quelques-uns:

  • Je n'utilise pas intentionnellement de jargon comme "transparence référentielle", "idempotence", etc. ils ne servent à rien en plus de semer la confusion, parlons donc d'immuabilité [a can't change that concept]. Erlang en tant que langue est une opinion, et il penche vers une forte immuabilité, alors qu'en Scala il est trop facile de faire en sorte que les acteurs changent/modifient leur état lorsqu'un message est reçu. Il n'est pas recommandé, mais la mutabilité dans Scala est juste devant vous, et les gens faites l'utilisez.

  • Un autre point intéressant dont Joe Armstrong parle est le fait que Scala/AKKA est limité par la JVM qui n'était tout simplement pas vraiment conçue en pensant à "être distribué", alors qu'Erlang VM l'était. a à voir avec de nombreuses choses telles que: l'isolement des processus, par processus par rapport à l'ensemble VM garbage collection, chargement de classe, planification de processus et autres.

Le but de ce qui précède n'est pas de dire que l'un est meilleur que l'autre, mais de montrer que la pureté du modèle d'acteur en tant que concept dépend de sa mise en œuvre.

Maintenant aux goroutines ..

Les goroutines aident à raisonner sur la concurrence séquentielle


Comme d'autres réponses déjà mentionnées, les goroutines prennent racine dans Communicating Sequential Processes , qui est un "langage formel pour décrire les modèles d'interaction dans les systèmes concurrents", qui par définition peut signifier à peu près n'importe quoi :)

Je vais donner des exemples basés sur core.async , car je connais mieux ses internes que les Goroutines. Mais core.async a été construit après le modèle Goroutines/CSP, donc il ne devrait pas y avoir trop de différences conceptuelles.

La principale primitive de concurrence dans core.async/Goroutine est un channel. Considérez un channel comme une "file d'attente sur les rochers". Ce canal est utilisé pour "passer" des messages. Tout processus qui souhaite "participer à un jeu" crée ou obtient une référence à un channel et place/prend (par exemple envoie/reçoit) des messages vers/depuis celui-ci.

Parking gratuit 24h/24

La plupart du travail effectué sur les canaux se fait généralement dans un " Goroutine " ou " go block ", qui " prend son corps et l'examine pour n'importe quel canal Il transformera le corps en une machine d'état. Une fois le blocage atteint, la machine d'état sera "parquée" et le thread de contrôle réel sera libéré. ​​Cette approche est similaire à celle utilisée dans C # async. Lorsque le blocage se termine, le code sera repris (sur un thread de pool de threads, ou le thread unique dans une machine virtuelle JS) "( source ).

Il est beaucoup plus facile de transmettre avec un visuel. Voici à quoi ressemble un blocage IO exécution:

blocking IO

Vous pouvez voir que les threads passent principalement du temps à attendre du travail. Voici le même travail mais effectué via l'approche "Goroutine"/"go block":

core.async

Ici, 2 threads ont fait tout le travail, que 4 threads ont fait dans une approche de blocage, tout en prenant le même temps.

Le kicker dans la description ci-dessus est: "les threads sont parqués " lorsqu'ils n'ont pas de travail, ce qui signifie que leur état est "déchargé" vers une machine d'état, et le thread JVM réel en direct est libre de faire d'autres travaux ( source pour un excellent visuel)

note: dans core.async, le canal peut être utilisé en dehors des "go block", qui seront soutenus par un thread JVM sans possibilité de parking: par ex. s'il bloque, il bloque le vrai thread.

Puissance d'une chaîne Go

Une autre chose énorme dans "Goroutines"/"go blocks" est les opérations qui peuvent être effectuées sur un canal. Par exemple, un timeout channel peut être créé, qui se fermera en X millisecondes. Ou sélectionnez/ alt! fonction qui, lorsqu'elle est utilisée en conjonction avec de nombreux canaux, fonctionne comme un mécanisme d'interrogation "êtes-vous prêt" sur différents canaux. Considérez-le comme un sélecteur de socket dans les E/S non bloquantes. Voici un exemple d'utilisation de timeout channel et alt! ensemble:

(defn race [q]
  (searching [:.yahoo :.google :.bing])
  (let [t (timeout timeout-ms)
        start (now)]
    (go
      (alt! 
        (GET (str "/yahoo?q=" q))  ([v] (winner :.yahoo v (took start)))
        (GET (str "/bing?q=" q))   ([v] (winner :.bing v (took start)))
        (GET (str "/google?q=" q)) ([v] (winner :.google v (took start)))
        t                          ([v] (show-timeout timeout-ms))))))

Cet extrait de code est tiré de wracer , où il envoie la même demande aux trois: Yahoo, Bing et Google, et renvoie un résultat de la plus rapide, ou expire (renvoie un message d'expiration) si aucun n'est retourné dans un délai donné. Clojure n'est peut-être pas votre première langue, mais vous ne pouvez pas être en désaccord sur la façon dont séquentielle cette implémentation de la simultanéité ressemble et se sent.

Vous pouvez également fusionner/fan-in/fan-out les données de/vers de nombreux canaux, mapper/réduire/filtrer/... les données des canaux et plus encore. Les chaînes sont également des citoyens de première classe: vous pouvez passer d'une chaîne à une chaîne.

Go UI Go!

Étant donné que core.async "go blocks" a cette capacité de "garer" l'état d'exécution, et d'avoir une "apparence et une sensation" très séquentielles lorsqu'il s'agit de la concurrence, qu'en est-il de JavaScript? Il n'y a pas de concurrence en JavaScript, car il n'y a qu'un seul thread, non? Et la façon dont la concurrence est mimée est via 1024 rappels.

Mais il ne doit pas en être ainsi. L'exemple ci-dessus de wracer est en fait écrit en ClojureScript qui se compile en JavaScript. Oui, cela fonctionnera sur le serveur avec de nombreux threads et/ou dans un navigateur: le code peut rester le même.

Goroutines contre core.async

Encore une fois, quelques différences de mise en œuvre [il y en a plus] pour souligner le fait que le concept théorique n'est pas exactement un à un dans la pratique:

  • Dans Go, un canal est tapé, dans core.async ce n'est pas: par exemple dans core.async, vous pouvez placer des messages de tout type sur le même canal.
  • Dans Go, vous pouvez mettre des objets modifiables sur un canal. Ce n'est pas recommandé, mais vous pouvez. Dans core.async, par Clojure design, toutes les structures de données sont immuables, donc les données à l'intérieur des canaux sont beaucoup plus sûres pour leur bien-être.

Alors quel est le verdict?


J'espère que ce qui précède fait la lumière sur les différences entre le modèle d'acteur et le CSP.

Pas pour provoquer une guerre des flammes, mais pour vous donner une autre perspective de, disons, Rich Hickey:

" Je reste peu enthousiaste à propos des acteurs. Ils couplent toujours le producteur au consommateur. Oui, on peut émuler ou implémenter certains types de files d'attente avec les acteurs (et, notamment, les gens le font souvent), mais puisque tout mécanisme d'acteur intègre déjà une file d'attente, il semble évident que les files d'attente sont plus primitives. Il convient de noter que les mécanismes de Clojure pour l'utilisation simultanée de l'état restent viables, et les canaux sont orientés vers les aspects de flux d'un système. "( source )

Cependant, dans la pratique, Whatsapp est basé sur Erlang OTP, et il semblait se vendre assez bien.

Une autre citation intéressante est de Rob Pike:

" Les envois en mémoire tampon ne sont pas confirmés à l'expéditeur et peuvent durer arbitrairement. Les canaux et les goroutines en mémoire tampon sont très proches du modèle d'acteur.

La vraie différence entre le modèle d'acteur et Go est que les chaînes sont des citoyens de première classe. Également important: ils sont indirects, comme les descripteurs de fichiers plutôt que les noms de fichiers, permettant des styles de concurrence qui ne sont pas aussi facilement exprimés dans le modèle d'acteur. Il y a aussi des cas où l'inverse est vrai; Je ne porte pas de jugement de valeur. En théorie, les modèles sont équivalents. "( source )

53
tolitius

Déplacer certains de mes commentaires vers une réponse. Cela devenait trop long: D (Ne pas enlever les messages de jamie et tolitius; ce sont deux réponses très utiles.)

Ce n'est pas tout à fait vrai que vous pourriez faire exactement les mêmes choses que vous faites avec des goroutines à Akka. Les canaux Go sont souvent utilisés comme points de synchronisation. Vous ne pouvez pas reproduire cela directement dans Akka. Dans Akka, le traitement post-synchronisation doit être déplacé dans un gestionnaire séparé ("parsemé", selon les mots de jamie: D). Je dirais que les modèles de conception sont différents. Vous pouvez lancer un goroutine avec un chan, faire des choses, puis <- attendre qu'il se termine avant de continuer. Akka a une forme moins puissante de cela avec ask, mais ask n'est pas vraiment la manière Akka IMO.

Les chans sont également saisis, contrairement aux boîtes aux lettres. C'est un gros problème OMI, et c'est assez choquant pour un système basé sur Scala. Je comprends que become est difficile à implémenter avec des messages dactylographiés, mais cela indique peut-être que become n'est pas très semblable à Scala. Je pourrais dire cela à propos d'Akka en général. Il se sent souvent comme son propre truc qui tourne sur Scala. Les goroutines sont une raison clé pour laquelle Go existe.

Ne vous méprenez pas; J'aime beaucoup le modèle d'acteur, et j'aime généralement Akka et je le trouve agréable à travailler. J'aime aussi généralement Go (je trouve Scala magnifique, tandis que je trouve Go simplement utile; mais il est assez utile).

Mais la tolérance aux pannes est vraiment le but d'Akka IMO. Il se trouve que vous obtenez la concurrence avec cela. La concurrence est au cœur des goroutines. La tolérance aux pannes est une chose distincte dans Go, déléguée à defer et recover, qui peut être utilisée pour implémenter un peu de tolérance aux pannes. La tolérance aux pannes d'Akka est plus formelle et riche en fonctionnalités, mais elle peut également être un peu plus compliquée.

Tout cela dit, malgré quelques similitudes passagères, Akka n'est pas un surensemble de Go, et ils présentent des divergences de fonctionnalités importantes. Akka et Go sont très différents dans la façon dont ils vous encouragent à aborder les problèmes, et les choses faciles dans l'un, sont maladroites, peu pratiques ou du moins non idiomatiques dans l'autre. Et ce sont les principaux facteurs de différenciation de tout système.

Donc, ramenez-le à votre vraie question: je recommanderais fortement de repenser l'interface Go avant de la porter à Scala ou Akka (qui sont également des choses assez différentes IMO). Assurez-vous que vous le faites la façon dont votre environnement cible signifie faire les choses. Un port direct d'une bibliothèque Go compliquée est susceptible de ne pas bien s'intégrer dans l'un ou l'autre environnement.

8
Rob Napier

Ce sont toutes des réponses excellentes et approfondies. Mais pour une façon simple de voir les choses, voici mon point de vue. Les goroutines sont une simple abstraction des acteurs. Les acteurs ne sont qu'un cas d'utilisation plus spécifique des Goroutines.

Vous pouvez implémenter des acteurs utilisant des Goroutines en créant la Goroutine à côté d'un canal. En décidant que la chaîne est "détenue" par ce Goroutine, vous dites que seul ce Goroutine en consommera. Votre Goroutine exécute simplement une boucle de correspondance de boîte de réception sur ce canal. Vous pouvez alors simplement passer la Manche comme "adresse" de votre "Acteur" (Goroutine).

Mais comme les Goroutines sont une abstraction, une conception plus générale que les acteurs, les Goroutines peuvent être utilisées pour beaucoup plus de tâches et de conceptions que les Acteurs.

Cependant, comme les acteurs sont un cas plus spécifique, les implémentations d'acteurs comme Erlang peuvent mieux les optimiser (récursivité ferroviaire sur la boucle de la boîte de réception) et peuvent fournir plus facilement d'autres fonctionnalités intégrées (acteurs multiprocessus et machines) .

6
DragonFax

peut-on dire que dans l'Actor Model, l'entité adressable est l'acteur, le destinataire du message. alors que dans les canaux Go, l'entité adressable est le canal, le canal dans lequel circule le message.

dans le canal Go, vous envoyez un message au canal, et un nombre illimité de destinataires peuvent être à l'écoute, et l'un d'eux recevra le message.

dans Acteur, un seul acteur à la référence de l'acteur auquel vous envoyez le message recevra le message.

1
weima