web-dev-qa-db-fra.com

Scala acteurs: recevoir vs réagir

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é à étudier Scala, qui semble être un langage très agréable.

Cependant, j'ai lu sur le framework Acteur de Scala dans Programmation dans Scala , et il y a une chose que je ne comprends pas . Dans le chapitre 30.4, il est dit que l'utilisation de react au lieu de receive permet de réutiliser les threads, ce qui est bon pour les performances, car les threads sont chers dans la JVM.

Est-ce à dire que, tant que je me souviens d'appeler react au lieu de receive, je peux démarrer 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 une goutte de sueur. Je détesterais le faire avec des fils Java. Quel type de limites est-ce que je regarde dans Scala par rapport à Erlang (et Java)?

En outre, comment fonctionne cette réutilisation de thread dans Scala? Supposons, pour plus de simplicité, que je n'ai qu'un seul fil. Est-ce que tous les acteurs que je commence s'exécuteront séquentiellement dans ce fil, ou y aura-t-il une sorte de changement de tâche? Par exemple, si je démarre deux acteurs qui se lancent des messages de ping-pong, est-ce que je risque un blocage s'ils sont démarrés dans le même thread?

Selon Programmation en Scala , écrire des acteurs pour utiliser react est plus difficile qu'avec receive. Cela semble plausible, car react ne revient pas. Cependant, le livre montre comment vous pouvez mettre un react dans 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 receive". Alors qu'est-ce que je manque ici? Qu'est-ce que receive peut faire que react ne peut pas, à part retourner? Et pourquoi je m'en soucie?

Enfin, pour venir au cœur de ce que je ne comprends pas: le livre ne cesse de mentionner comment l'utilisation de react permet 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 renvoyant (receive)?

J'ai l'impression que La programmation en Scala a ignoré certains des problèmes clés ici, ce qui est dommage, car sinon c'est un livre vraiment excellent.

110
jqno

Tout d'abord, chaque acteur attendant receive occupe un thread. S'il ne reçoit jamais rien, ce thread ne fera rien. Un acteur sur react n'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. Un thread de réception devrait renvoyer quelque chose, pas un thread de réaction. Ainsi, l'état de pile précédent à la fin du dernier react peut être, et est, entièrement supprimé. Ne pas avoir besoin d'enregistrer ou de restaurer l'état de la pile accélère le démarrage du thread.

Il existe différentes raisons de performances pour lesquelles vous pouvez souhaiter l'une ou l'autre. Comme vous le savez, avoir trop de threads dans Java n'est pas une bonne idée. D'un autre côté, parce que vous devez attacher un acteur à un thread avant qu'il ne puisse react, il est plus rapide de receive un message que de react. Donc, si vous avez des acteurs qui reçoivent beaucoup de messages mais en font très peu, le délai supplémentaire de react peut rendre trop lent pour vos besoins.

78
Daniel C. Sobral

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 "concurrent" programme dans un seul thread (essayez de définir la propriété système actors.maxPoolSize découvrir).

L'une des raisons les plus évidentes pour lesquelles il est nécessaire de supprimer la pile d'appels est que sinon la méthode loop se terminerait par un StackOverflowError. En l'état, le framework termine plutôt intelligemment un react en lançant un SuspendActorException, qui est capté par le code de boucle qui exécute ensuite le react via le andThen méthode.

Jetez un œil à la méthode mkBody dans Actor puis à la méthode seq pour voir comment la boucle se replanifie - des trucs terriblement intelligents!

21
oxbow_lakes

Ces déclarations de "jeter la pile" m'ont aussi 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 thread complète est disponible et prête à continuer à partir du point "d'attente" lors de 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 de "10" qui est dans le cadre de pile avant le thread bloqué.

En cas de réaction, il n'y a pas de thread dédié, le corps entier de la méthode de réaction 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 déclarations qui peuvent être capturées comme une fermeture seule seront exécutées et c'est là que le type de retour "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 vide, cela signifierait qu'il est légal d'avoir des instructions 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é, 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). Étant donné que le contrat de react a le type de retour "Nothing", il ne peut y avoir aucune instruction après react, et il n'y a aucune raison de maintenir la pile. Dans l'exemple ci-dessus, la variable "a" ne devrait pas être conservée 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, afin qu'elles puissent s'exécuter très bien.

Le Java framework d'acteur Kilim fait en fait la maintenance de la pile en enregistrant la pile qui se déroule sur la réaction recevant un message.

20
Ashwin

Juste pour l'avoir ici:

Programmation basée sur les événements sans inversion de contrôle

Ces articles sont liés à partir de l'API scala pour Actor) et fournissent le cadre théorique pour la mise en œuvre de l'acteur. Cela comprend pourquoi il est possible que react ne revienne jamais.

8
Hexren

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

Cela m'amène à penser que réagir est meilleur car il suggère au thread actuel de considérer d'autres acteurs pour la planification où la réception "pourrait" engager le thread actuel pour continuer à exécuter d'autres messages pour le même acteur.

0
user3407472