Acteurs Scala: recevoir vs réagir


110

Permettez-moi d'abord de dire que j'ai beaucoup d'expérience Java, mais que je ne me suis intéressé que récemment aux langages fonctionnels. Récemment, j'ai commencé à regarder Scala, qui semble être un très bon langage.

Cependant, j'ai lu sur le framework Actor de Scala dans Programming in Scala , et il y a une chose que je ne comprends pas. Au chapitre 30.4, il est dit que l'utilisation reactau lieu de receivepermet de réutiliser les threads, ce qui est bon pour les performances, puisque les threads sont chers dans la JVM.

Cela signifie-t-il que, tant que je me souviens d'appeler reactau lieu de receive, je peux créer autant d'acteurs que je veux? Avant de découvrir Scala, j'ai joué avec Erlang, et l'auteur de Programming Erlang se vante de générer plus de 200 000 processus sans transpirer. Je détesterais faire ça avec les threads Java. Quel genre de limites est-ce que je regarde dans Scala par rapport à Erlang (et Java)?

En outre, comment cette réutilisation de thread fonctionne-t-elle dans Scala? Supposons, pour simplifier, que je n'ai qu'un seul fil. Tous les acteurs que je commence seront-ils exécutés séquentiellement dans ce fil, ou une sorte de changement de tâche aura-t-elle lieu? Par exemple, si je démarre deux acteurs qui se transmettent des messages ping-pong, est-ce que je risque une impasse s'ils sont démarrés dans le même thread?

Selon Programming in Scala , écrire des acteurs à utiliser reactest plus difficile qu'avec receive. Cela semble plausible, car reactne revient pas. Cependant, le livre continue en montrant comment vous pouvez mettre un reactdans une boucle en utilisant Actor.loop. En conséquence, vous obtenez

loop {
    react {
        ...
    }
}

qui, pour moi, semble assez similaire à

while (true) {
    receive {
        ...
    }
}

qui est utilisé plus tôt dans le livre. Pourtant, le livre dit que "dans la pratique, les programmes auront besoin d'au moins quelques-uns receive". Alors qu'est-ce que je manque ici? Que peut receivefaire qui ne reactpeut pas, à part revenir? Et pourquoi je m'en soucie?

Enfin, revenons au cœur de ce que je ne comprends pas: le livre ne cesse de mentionner comment l'utilisation reactpermet de supprimer la pile d'appels pour réutiliser le thread. Comment ça marche? Pourquoi est-il nécessaire de supprimer la pile d'appels? Et pourquoi la pile d'appels peut-elle être supprimée lorsqu'une fonction se termine en lançant une exception ( react), mais pas lorsqu'elle se termine en retournant ( receive)?

J'ai l'impression que Programming in Scala a passé sous silence certains des problèmes clés ici, ce qui est dommage, car sinon, c'est un livre vraiment excellent.


Réponses:


78

Tout d'abord, chaque acteur qui attend receiveoccupe un fil. S'il ne reçoit jamais rien, ce fil ne fera jamais rien. Un acteur sur reactn'occupe aucun thread jusqu'à ce qu'il reçoive quelque chose. Une fois qu'il reçoit quelque chose, un thread lui est alloué et il y est initialisé.

Maintenant, la partie initialisation est importante. On s'attend à ce qu'un thread de réception retourne quelque chose, pas un thread qui réagit. Donc, l'état précédent de la pile à la fin du dernierreact peut être, et est, entièrement ignoré. Ne pas avoir besoin d'enregistrer ou de restaurer l'état de la pile accélère le démarrage du thread.

Il y a plusieurs raisons de performances pour lesquelles vous pourriez vouloir l'un ou l'autre. Comme vous le savez, avoir trop de threads en Java n'est pas une bonne idée. D'autre part, comme il faut attacher un acteur à un fil avant qu'il ne le puisse react, il est plus rapide à receiveun message qu'à reactlui. Donc, si vous avez des acteurs qui reçoivent beaucoup de messages mais en font très peu, le délai supplémentaire de reactpeut le rendre trop lent pour vos besoins.


21

La réponse est "oui" - si vos acteurs ne bloquent rien dans votre code et que vous utilisez react, alors vous pouvez exécuter votre programme "concurrent" dans un seul thread (essayez de définir la propriété système actors.maxPoolSizepour le savoir).

L'une des raisons les plus évidentes pour lesquelles il est nécessaire de supprimer la pile d'appels est que sinon, la loopméthode se terminerait par un StackOverflowError. Dans l'état actuel des choses, le framework termine plutôt intelligemment un reacten lançant un SuspendActorException, qui est capturé par le code en boucle qui exécute ensuite le à reactnouveau via leandThen méthode.

Jetez un œil à la mkBodyméthode Actorpuis à la seqméthode pour voir comment la boucle se replanifie d'elle-même - des choses terriblement intelligentes!


20

Ces déclarations de "rejeter la pile" m'ont également troublé pendant un certain temps et je pense que je comprends maintenant et c'est ce que je comprends maintenant. En cas de "réception", il y a un blocage de thread dédié sur le message (en utilisant object.wait () sur un moniteur) et cela signifie que la pile de threads complète est disponible et prête à continuer à partir du point "d'attente" à la réception d'un message. Par exemple si vous aviez le code suivant

  def a = 10;
  while (! done)  {
     receive {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after receive and printing a " + a)
  }

le thread attendrait dans l'appel de réception jusqu'à ce que le message soit reçu, puis continuerait et imprimerait le message "après réception et impression d'un 10" et avec la valeur "10" qui se trouve dans le cadre de pile avant que le thread ne soit bloqué.

En cas de réaction, il n'y a pas de tel thread dédié, tout le corps de la méthode react est capturé comme une fermeture et est exécuté par un thread arbitraire sur l'acteur correspondant recevant un message. Cela signifie que seules les instructions qui peuvent être capturées en tant que clôture seules seront exécutées et c'est là que le type de retour de "Nothing" entre en jeu. Considérez le code suivant

  def a = 10;
  while (! done)  {
     react {
        case msg =>  println("MESSAGE RECEIVED: " + msg)
     }
     println("after react and printing a " + a) 
  }

Si react avait un type de retour nul, cela signifierait qu'il est légal d'avoir des déclarations après l'appel "react" (dans l'exemple l'instruction println qui imprime le message "après react et impression d'un 10"), mais en réalité que ne serait jamais exécuté car seul le corps de la méthode "react" est capturé et séquencé pour une exécution ultérieure (à l'arrivée d'un message). Puisque le contrat de react a le type de retour "Nothing", il ne peut y avoir aucune instruction suivant react, et il n'y a aucune raison de maintenir la pile. Dans l'exemple ci-dessus, la variable "a" ne devrait pas être maintenue car les instructions après les appels de réaction ne sont pas exécutées du tout. Notez que toutes les variables nécessaires par le corps de react sont déjà capturées en tant que fermeture, donc il peut s'exécuter très bien.

Le framework d'acteur java Kilim effectue en fait la maintenance de la pile en sauvegardant la pile qui se déroule sur le React recevant un message.


Merci, c'était très instructif. Mais ne voulez-vous pas dire +adans les extraits de code, au lieu de +10?
jqno

Très bonne réponse. Je ne comprends pas non plus.
santiagobasulto


0

Je n'ai pas fait de travail majeur avec scala / akka, mais je comprends qu'il y a une différence très significative dans la façon dont les acteurs sont programmés. Akka est juste un pool de threads intelligent qui tranche dans le temps l'exécution des acteurs ... Chaque tranche de temps sera une exécution de message jusqu'à la fin par un acteur contrairement à Erlang qui pourrait être par instruction?!

Cela m'amène à penser que la réaction est meilleure car elle suggère au thread actuel de considérer d'autres acteurs pour la planification alors que recevoir "pourrait" engager le thread actuel pour continuer à exécuter d'autres messages pour le même acteur.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.