scala multithreading actor

Actores Scala: recibir vs reaccionar



multithreading (5)

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

Sin embargo, he estado leyendo sobre el marco Actor de Scala en Programming in Scala , y hay una cosa que no entiendo. En el capítulo 30.4 dice que usar react lugar de receive hace posible reutilizar los hilos, lo que es bueno para el rendimiento, ya que los hilos son caros en la JVM.

¿Esto significa que, mientras recuerde llamar a react lugar de receive , puedo comenzar tantos actores como quiera? Antes de descubrir a Scala, he estado jugando con Erlang, y el autor de Programming Erlang se jacta de engendrar más de 200,000 procesos sin sudar nada. Odiaría hacer eso con los hilos de Java. ¿Qué tipo de límites estoy viendo en Scala en comparación con Erlang (y Java)?

Además, ¿cómo reutiliza este hilo de trabajo en Scala? Supongamos, por simplicidad, que tengo solo un hilo. ¿Todos los actores que empiezo se ejecutarán secuencialmente en este hilo, o se producirá algún tipo de cambio de tareas? Por ejemplo, si inicio dos actores con los mensajes ping-pong, ¿me arriesgaré a un punto muerto si se inician en el mismo hilo?

Según Programación en Scala , escribir a los actores para usar react es más difícil que con receive . Esto suena plausible, ya que la react no regresa. Sin embargo, el libro continúa para mostrar cómo puedes react dentro de un bucle con Actor.loop . Como resultado, obtienes

loop { react { ... } }

que, para mí, parece bastante similar a

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

que se usa anteriormente en el libro. Aún así, el libro dice que "en la práctica, los programas necesitarán al menos unos pocos receive ". Entonces, ¿qué es lo que echo de menos aquí? ¿Qué puede receive hacer que react no puede, 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 de react hace 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é la pila de llamadas puede descartarse cuando una función termina lanzando una excepción ( react ), pero no cuando finaliza al regresar ( receive )?

Tengo la impresión de que Programación en Scala ha pasado por alto algunos de los temas clave aquí, lo cual es una lástima, porque sino es un libro verdaderamente excelente.


Esas declaraciones de "descartar la pila" también me confundieron por un tiempo y creo que ahora lo entiendo y ahora comprendo esto. En el caso de "recibir" hay un bloqueo de hilos dedicado en el mensaje (usando object.wait () en un monitor) y esto significa que la pila de hilos completa está disponible y lista para continuar desde el punto de "espera" al recibir un mensaje. Por ejemplo, si tuvieras 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 pila antes de que se bloquee el hilo.

En caso de que no exista tal hilo dedicado, todo el cuerpo del método del método de reacción se captura como un cierre y se ejecuta mediante un hilo arbitrario en el actor correspondiente que recibe un mensaje. Esto significa que solo se ejecutarán las declaraciones que se pueden capturar como cierre solo y ahí es donde entra en juego el tipo de devolución de "Nada". Considera el siguiente código

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

Si reaccionar tuviera un tipo de vacío devuelto, significaría que es legal tener declaraciones después de la llamada de "reacción" (en el ejemplo, la sentencia println que imprime el mensaje "después de reaccionar e imprimir un 10"), pero en realidad nunca se ejecutará ya que solo el cuerpo del método "reaccionar" se captura y se secuencia para su posterior ejecución (a la llegada de un mensaje). Dado que el contrato de reacción tiene el tipo de devolución de "Nada", no puede haber ningún enunciado 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 después de las llamadas de reacción no se ejecutan en absoluto. Tenga en cuenta que todas las variables necesarias por el cuerpo de reacción ya se capturan como un cierre, por lo que puede ejecutarse bien.

El framework de java actor Kilim realidad hace el mantenimiento de la pila al guardar la pila que se desenrolla en la reacción de recibir un mensaje.


La respuesta es "sí": si tus actores no están bloqueando nada en tu código y estás usando react , entonces puedes ejecutar tu programa "concurrente" en un solo hilo (prueba configurando la propiedad del sistema actors.maxPoolSize para averiguarlo) .

Una de las razones más obvias por las que es necesario descartar la pila de llamadas es que, de lo contrario, el método de loop finalizaría en Error . Tal como están las cosas, el marco termina inteligentemente por react SuspendActorException una SuspendActorException , que es captada por el código de bucle que luego ejecuta la react nuevamente mediante el método andThen .

Eche un vistazo al método mkBody en Actor y luego al método seq para ver cómo el ciclo se reprograma a sí mismo: ¡cosas terriblemente inteligentes!


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 subproceso inteligente que es la ejecución de los actores en rebanadas de tiempo ... Cada vez que un trozo será una ejecución de mensaje completada por un actor a diferencia de Erlang, ¿qué podría ser por instrucción?

Esto me lleva a pensar que la reacción es mejor, ya que insinúa el hilo actual para considerar a otros actores para programar donde recibir "poder" involucrar al hilo actual para continuar ejecutando otros mensajes para el mismo actor.


Primero, cada actor que espera receive recibe un hilo. Si nunca recibe nada, ese hilo nunca hará nada. Un actor en react no 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 receptor devuelva algo, un hilo que reacciona no lo es. Entonces, el estado de pila anterior al final de la última react puede ser, y es, completamente descartado. No es necesario guardar ni restaurar el estado de pila, lo que hace que el subproceso sea más rápido para comenzar.

Hay varios motivos de rendimiento por los que podría querer uno u otro. Como saben, tener demasiados hilos en Java no es una buena idea. Por otro lado, como debe unir un actor a un hilo antes de que pueda react , es más rápido receive un mensaje que react ante él. Entonces, si tiene actores que reciben muchos mensajes pero hacen muy poco con ellos, el retraso adicional de la react puede hacer que sea demasiado lento para sus propósitos.