Actores de Scala: recibir vs reaccionar


110

Permítanme decir primero que tengo mucha experiencia en Java, pero solo recientemente me he interesado en los lenguajes funcionales. Recientemente comencé a buscar en Scala, que parece un lenguaje muy agradable.

Sin embargo, he estado leyendo sobre el marco Actor de Scala en Programación en Scala , y hay una cosa que no entiendo. En el capítulo 30.4 dice que usar en reactlugar de receivepermite reutilizar subprocesos, lo cual es bueno para el rendimiento, ya que los subprocesos son costosos en la JVM.

¿Significa esto que, siempre que me acuerde de llamar en reactlugar de receive, puedo iniciar tantos Actores como quiera? Antes de descubrir Scala, he estado jugando con Erlang, y el autor de Programming Erlang se jacta de generar más de 200.000 procesos sin sudar. Odiaría hacer eso con los hilos de Java. ¿Qué tipo de límites estoy mirando en Scala en comparación con Erlang (y Java)?

Además, ¿cómo funciona la reutilización de este hilo en Scala? Supongamos, por simplicidad, que solo tengo un hilo. ¿Todos los actores que empiezo se ejecutarán secuencialmente en este hilo, o se producirá algún tipo de cambio de tarea? Por ejemplo, si comienzo a dos actores que se envían mensajes de ping-pong entre sí, ¿correré el riesgo de un punto muerto si comienzan en el mismo hilo?

Según Programming in Scala , escribir actores para usar reactes más difícil que con receive. Esto suena plausible, ya reactque no regresa. Sin embargo, el libro continúa mostrando cómo se puede poner reactdentro de un bucle usando Actor.loop. Como resultado, obtienes

loop {
    react {
        ...
    }
}

que, para mí, parece bastante similar a

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

que se utiliza anteriormente en el libro. Aún así, el libro dice que "en la práctica, los programas necesitarán al menos algunos receive". Entonces, ¿qué me estoy perdiendo aquí? ¿Qué puede receivehacer que reactno pueda, además de regresar? ¿Y por qué me importa?

Finalmente, llegando al núcleo de lo que no entiendo: el libro sigue mencionando cómo el uso reacthace posible descartar la pila de llamadas para reutilizar el hilo. ¿Cómo funciona? ¿Por qué es necesario descartar la pila de llamadas? ¿Y por qué se puede descartar la pila de llamadas cuando una función termina lanzando una excepción ( react), pero no cuando termina devolviendo ( receive)?

Tengo la impresión de que Programming in Scala ha pasado por alto algunos de los problemas clave aquí, lo cual es una pena, porque de lo contrario es un libro realmente excelente.


Respuestas:


78

Primero, cada actor que espera receiveocupa un hilo. Si nunca recibe nada, ese hilo nunca hará nada. Un actor en reactno ocupa ningún hilo hasta que recibe algo. Una vez que recibe algo, se le asigna un hilo y se inicializa en él.

Ahora, la parte de inicialización es importante. Se espera que un hilo de recepción devuelva algo, un hilo de reacción no lo es. Por lo tanto, el estado de la pila anterior al final del último reactpuede descartarse por completo. No es necesario guardar ni restaurar el estado de la pila, lo que acelera el inicio del hilo.

Hay varias razones de rendimiento por las que podría querer una u otra. Como sabe, tener demasiados hilos en Java no es una buena idea. Por otro lado, debido a que debe adjuntar un actor a un hilo antes de que pueda react, es más rápido con receiveun mensaje que reactcon él. Por lo tanto, si tiene actores que reciben muchos mensajes pero hacen muy poco con ellos, el retraso adicional reactpuede hacer que sea demasiado lento para sus propósitos.


21

La respuesta es "sí": si sus actores no están bloqueando nada en su código y usted lo está usando react, entonces puede ejecutar su programa "concurrente" dentro de un solo hilo (intente configurar la propiedad del sistema actors.maxPoolSizepara averiguarlo).

Una de las razones más obvias por las que es necesario descartar la pila de llamadas es que, de lo contrario, el loopmétodo terminaría en a StackOverflowError. Tal como están las cosas, el marco termina inteligentemente a reactlanzando a SuspendActorException, que es capturado por el código de bucle que luego se ejecuta de reactnuevo a través del andThenmétodo.

Eche un vistazo al mkBodymétodo en Actory luego al seqmétodo para ver cómo el ciclo se reprograma a sí mismo: ¡cosas terriblemente inteligentes!


20

Esas declaraciones de "descartar la pila" me confundieron también por un tiempo y creo que lo entiendo ahora y este es mi entendimiento ahora. En caso de "recibir" hay un bloqueo de hilo dedicado en el mensaje (usando object.wait () en un monitor) y esto significa que la pila de hilo completa está disponible y lista para continuar desde el punto de "esperar" al recibir un mensaje. Por ejemplo, si tuviera el siguiente código

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

el hilo esperaría en la llamada de recepción hasta que se reciba el mensaje y luego continuaría e imprimiría el mensaje "después de recibir e imprimir un 10" y con el valor de "10" que está en el marco de la pila antes de que el hilo se bloquee.

En caso de reaccionar no hay tal hilo dedicado, todo el cuerpo del método del método de reacción se captura como un cierre y es ejecutado por algún hilo arbitrario en el actor correspondiente que recibe un mensaje. Esto significa que solo se ejecutarán aquellas declaraciones que se puedan capturar como un cierre y ahí es donde entra en juego el tipo de retorno de "Nada". Considere el siguiente código

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

Si react tuviera un tipo de retorno de void, significaría que es legal tener declaraciones después de la llamada "react" (en el ejemplo, la declaración println que imprime el mensaje "después de reaccionar e imprimir un 10"), pero en realidad eso nunca se ejecutaría, ya que solo el cuerpo del método "react" se captura y se secuencia para su ejecución más tarde (a la llegada de un mensaje). Dado que el contrato de react tiene el tipo de retorno de "Nada", no puede haber ninguna declaración después de reaccionar, y no hay razón para mantener la pila. En el ejemplo anterior, la variable "a" no tendría que mantenerse, ya que las declaraciones posteriores a las llamadas de reacción no se ejecutan en absoluto. Tenga en cuenta que todas las variables necesarias por el cuerpo de react ya se capturaron como un cierre, por lo que se puede ejecutar sin problemas.

El marco del actor Java Kilim en realidad hace el mantenimiento de la pila al guardar la pila que se desenrolla en la reacción y recibe un mensaje.


Gracias, eso fue muy informativo. ¿Pero no quiso decir +aen los fragmentos de código, en lugar de +10?
jqno

Gran respuesta. Yo tampoco lo entiendo.
santiagobasulto


0

No he hecho ningún trabajo importante con scala / akka, sin embargo, entiendo que hay una diferencia muy significativa en la forma en que se programan los actores. Akka es solo un grupo de hilos inteligente que es la ejecución de actores en el tiempo ... Cada segmento será una ejecución de mensaje hasta su finalización por parte de un actor a diferencia de Erlang, que podría ser por instrucción.

Esto me lleva a pensar que reaccionar es mejor, ya que sugiere que el hilo actual considere a otros actores para la programación donde, como recibir, "podría" involucrar el hilo actual para continuar ejecutando otros mensajes para el mismo actor.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.