HolaMundo en Scala II: Actores

Apenas ayer publiqué mis pininos en Scala. He seguido leyendo un poco al respecto y después hice una segunda versión, con una clase menos, que fue sustituida por una función.

Ahora hice una tercera versión, usando actores. Esto de los actores ya seguramente el Dr. Ferro nos lo explicará mejor en la serie que está escribiendo de Scala. Los actores es la manera en que se maneja concurrencia de manera sencilla en Scala. Aquí me doy cuenta que sí tomaron algunas cosas prestadas de Erlang, un lenguaje de programación funcional creado expresamente para programación concurrente que alguna vez empecé a aprender pero por falta de práctica (porque no tenía un proyecto real que pudiera realizar con él) lo dejé.

Ahora que veo Scala, el modelo de comunicación entre procesos es muy similar, obviamente con algunas diferencias importantes: En Erlang se manejan procesos ligeros, que no corresponden con hilos como los conocemos; la comunicación entre procesos es muy rápida y los procesos ocupan muy poca memoria, por lo que se pueden tener varios miles de procesos corriendo en una aplicación Erlang sin que represente un alto costo al hardware (en términos de CPU/RAM); Erlang no tiene variables, en el sentido de que sean modificables; cuando se crea una variable, y se le asigna un valor, ya así se queda, no se puede modificar.

Scala corre sobre la JVM (y creo que sobre el CLR de .NET también, pero aquí nos enfocamos obviamente a la JVM), por lo que está amarrado a los Threads de la JVM, al modelo de manejo de excepciones de la JVM, etc. En Scala sí hay variables, y además tenemos valores inmutables, que se asignan una vez nada más y no se pueden volver a modificar. Y en Scala, en vez de tener procesos ligeros, tenemos actores. El objetivo es que los actores se puedan pasar mensajes entre ellos, de manera asíncrona, de manera que no tenemos que estar manejando hilos (de eso se encargará el runtime de Scala, que parece que ejecuta todo en un ThreadPool de tamaño variable).

Hay toda una sintaxis para pasar mensajes entre actores y hay varias palabras reservadas para poder manejarlos. Este artículo no pretende ser un tratado o tutorial sobre los actores en Scala, es simplemente documentar mi primer contacto con este lenguaje. Lo primero que hay que hacer es definir los mensajes que vamos a manejar. Y antes de lo primero, tenemos que tener una idea clara de cómo vamos a implementar las cosas. Así que veamos cómo ha ido evolucionando nuestra implementación:

Primero, en Java (y en la primera implementación en Scala), tenemos un Servidor aceptando conexiones TCP en un ciclo infinito. Cada que se recibe una nueva conexión, se crea un objeto que implementa Runnable para manejar el Socket, y se echa a andar en un Thread separado; este objeto toma los streams del socket y los pasa a un tercer objeto que hace el trabajo de leer un renglón de texto e imprimir una línea de texto con un saludo. Posteriormente el Runnable cierra el socket y con eso termina su ejecución.

La siguiente implementación en Scala tiene también al servidor aceptando conexiones TCP en un ciclo infinito, pero ahora cuando recibe una nueva conexión, crea un Runnable al que le pasa dos parámetros: el socket recién aceptado, y una función que recibe como parámetros un InputStream y un OutputStream, y que devuelve un String. Esa función la definimos en otra parte, y hace el trabajo de leer una línea de texto y luego imprimir un saludo. El Runnable en este caso ejecuta la función pasándole como parámetros los streams del socket y luego lo cierra. Este Runnable se ejecuta en un Thread creado por el Servidor.

¿Cómo debería ser la siguiente implementación? Pues creo yo que no deberíamos estar lidiando ya directamente con Threads. Lo que debería hacer el Servidor es aceptar una conexión, pasarla a un actor y echarlo a andar. Ese actor debe de hacer todo el trabajo: leer la linea de texto y luego imprimir el resultado.

Ya que estamos familiarizados con el ejemplo del servidor holamundo, ahora podemos ver esta nueva implementación. Primero que nada, el actor, que no es otra cosa que una subclase de scala.actors.Actor, y debemos sobreescribir el método  :

 

Esto a fin de cuentas se parece muchísimo a implementar un Runnable; nuestro actor tiene un constructor que recibe un Socket lo único que hace es lo de siempre: leer una línea, imprimir una línea, cerrar socket. Y cómo se usa? Pues en este caso, igualito que un Thread. Nuestro ciclo en el Servidor cambia muy poco:

 

Si lo único que queríamos era eliminar el uso directo de Threads, misión cumplida.

Dos Actores

Al principio mencioné que había sintaxis especial para pasar mensajes entre actores, sin embargo no utilicé nada de eso en esta primera implementación, por la sencilla razón de que solamente hice un actor, y estoy creando un actor para cada socket.

Una manera de usar los mensajes, es que nuestro actor sea un singleton. Entonces ya no se crea una instancia para cada socket, sino que siempre usamos el mismo actor, al cual le debemos enviar un mensaje... y el socket será el mensaje.

El código para recibir un mensaje debe estar dentro de un ciclo y ahí vamos a poner una especie de   para los distintos mensajes que vamos a recibir; ahí se hace pattern-matching muy similar al de Erlang, donde se coteja el mensaje recibido contra los casos contemplados y si uno se cumple, ese se ejecuta:

 

Y ahora, modificamos el Servidor para que mande un mensaje al actor. Pero antes, es importante echar a andar al actor, de otra forma el mensaje no le va a llegar (porque no se ha ejecutado el método  ):

 

Aquí vemos nueva sintaxis para los mensajes (esta sí me consta que fue directamente tomada de Erlang): Ese ! significa enviar un mensaje al actor. La sintaxis completa es  , y el mensaje puede ser cualquier objeto; en este caso estamos usando el socket como mensaje.

He escuchado y leído interminables veces que en Scala hay muchas maneras distintas de hacer lo mismo. Y este caso, por simple que parezca, no es la excepción; además, me he dado cuenta que cuando dicen eso, aunque muchas veces se refieren a la sintaxis de cómo escribir una sentencia o definir una función, pues también aplica para el diseño de los componentes, de la manera de hacer las cosas no solamente en cuanto a la sintaxis; hasta ahora ya llevo 4 maneras de implementar el servidor de holamundo, y honestamente no sé cuál sea mejor.

Y eso no es todo: todavía hice otras 3 maneras de implementar lo mismo, usando también actores. La que muestro a continuación se parece a la primera, es decir un actor por socket, pero ahora son DOS actores por socket. Simplemente partí en dos el actor que originalmente hacía todo; ahora lo que hace ese actor es primero crear un actor distinto para leer del socket, echarlo a andar, y esperar a que ese actor le avise cuando haya leido para escribir al socket. En este caso, sigue habiendo un solo mensaje: El que el lector le mandará al escritor, y dado que es algo muy simple, usaremos el texto leído como mensaje. Entonces, el lector lo hacemos de esta forma:

 

Y el proceso original queda así después de unas modificaciones:

 

El Servidor queda exactamente igual que el del primer ejemplo que puse con actores. Evidentemente no es el mejor ejemplo de actores porque el flujo es muy simple: el Proceso arranca el lector y solamente está esperando un evento, y al recibirlo ejecuta su tarea y termina. Pero si queremos hacer como en el segundo ejemplo y manejar ahora ambos actores como singletons, entonces ya tendremos que manejar más mensajes.

Dos actores singleton

Algo que resalta de este modelo de actores, es que la sintaxis es realmente simple. El signo de admiración lo podemos ver como que simplemente está invocando código del actor especificado, en un hilo separado, con el mensaje que le pasamos. Y el ciclo de loop-react simplemente espera a que se invoquen estos eventos. Si tenemos un actor stateless, podrá recibir varios eventos de manera concurrente y ejecutar cada uno sin interrumpir a los demás; sin embargo esto es casi transparente al programador, puesto que estamos creando hilos, ni usando cosas como bloques synchronized, o los métodos wait/notify que confunden a más de uno.

Aquí el ejemplo con dos actores singleton. En este caso, el servidor le pasará el socket recién recibido como mensaje al Lector, para que comience ahí la lectura; en cuanto el Lector haya leído su texto, debe mandar ese texto como mensaje al Escritor. Dado que son singletons, no deben terminar ese ciclo nunca, si es que queremos tener el servidor corriendo indefinidamente.

Y aquí entra una nueva dificultad: El servidor pasará el socket como mensaje al Lector. Y el Lector, pues aparte del nombre que lea de ese socket, también debe pasar el socket mismo al Escritor, para que pueda imprimir ahí el saludo; el Escritor no puede tener referencia al socket porque debe ser stateless para poderlo mantener como singleton. Ya mencioné que un mensaje entre actores puede ser cualquier objeto, pero aquí estamos hablando de dos objetos. Es evidente que necesitamos un contenedor, un objeto que guarde el socket y el nombre, para pasarlo como mensaje del Lector al Escritor.

Podemos definir una clase para esto, ya vimos que en Scala es sencillo porque podemos definir el constructor de la clase en la declaración de la misma; y entonces sólo tendríamos que construir una instancia de dicha clase con los objetos que queremos pasar. Esta clase no necesita tener métodos; solamente accesores para que el receptor del mensaje pueda obtener lo que necesita. Suena bastante sencillo; pero, Scala nos facilita las cosas aún más: si necesitamos definir una clase que vamos a usar únicamente como mensaje, existe una sintaxis especial:

 

Al anteponerle   a una declaración de clase, estamos indicando que se va a usar como mensaje, y por lo tanto no necesitamos ponerle cuerpo; es suficiente con indicar el nombre de la clase y los argumentos que necesita recibir su constructor.

Veamos ahora la implementación del Lector, que es el más sencillo y es donde comienza la acción, puesto que lo primero que hay que hacer con una nueva conexión es leer del socket y después avisamos al Escritor:

 

Hasta aquí lo único nuevo es ver cómo se usa una case class, para enviar un mensaje. Veamos ahora cómo se usa cuando se recibe un mensaje:

 

Y por último, el Servidor, que simplemente debe arrancar ambos actores y luego pasar los sockets recibidos al Lector. Les pongo el código completo de Servidor.scala, porque quiero mostrar un par de cosas más:

 

Con esto hemos visto varias maneras de pasar mensajes entre actores. Pasar mensajes es ridículamente sencillo, y recibir mensajes no es nada complicado. Si pensamos además que esto resulta en que se ejecuta código en distintos hilos, y que de hecho también hay sincronización entre hilos, nos daremos cuenta de lo sencillo que resulta hacer aplicaciones concurrentes con Scala. Definitivamente mejor alternativa que estar manejando hilos de manera manual en Java; simplemente la alternativa de los actores singleton es prácticamente imposible de implementar en Java, por la simple razón de que no se pueden pasar mensajes; la única manera de lograr algo así en Java es que se haga una instancia de Lector por cada socket, porque esa instancia tendrá que guardar referencia al Socket que va a usar; la única optimización sería usar un ThreadPool para ejecutar los Lectores en vez de crear un Thread para cada Lector.

 

Este manejo tan sencillo, la abstracción que ofrece el modelo de actores para manejar situaciones de concurrencia y procesamiento simultáneo, creo que son una de las mayores ventajas de Scala sobre Java; el hecho de que Scala sea funcional y tenga otras monerías como los Traits también es muy bueno, pero por ejemplo en todas estas variantes de actores, no usé un estilo funcional, no se ve complicado, creo que para cualquier programador Java que haya hecho tenido que manejar algo de concurrencia le quedará muy claro que esta alternativa es mejor.

ACTUALIZACIÓN: El código completo para todos estos ejemplos ya está disponible en GitHub.

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Al anteponerle case a una

Al anteponerle case a una declaración de clase, estamos indicando que se va a usar como mensaje

No exactamente, pero sí que se puede usar en el pattern matching ( es básicamente un instanceof glorificado - y simplificado - )

Con el   aún se puede poner cuerpo, lo que hacen estas case clases es crear muchísimos método auxiliares por tí además de los consabidos (¿?)   y   , como por ejemplo una función con el nombre de la clase para que puedas prescindir del "new" ejemplo:
 

Super el post y los ejemplos.

+3 :)

p.d. Es "costumbre" omitir los paréntesis en Scala para los métodos que no tienen efectos secudarios, pero mantenerlos para los que sí ( ejemplo el método readLine debe de llevalors   ) Se podría decir que en Scala las funciones no llevan paréntesis y los métodos sí? :) :)

Imagen de ezamudio

ok

es bueno saber lo de los métodos. Una convención útil.

Imagen de greeneyed

Interesante

Muy interesante, gracias. La parte más "compleja" de realiza en Java sería, IMHO, el paso de mensajes, ya que la implementación en Singleton no es tan complicada de hacer si simplemente el trabajo no lo realizan las instancias de Runnable si no simplemente las usas para llamar a un Singleton de otra clase con los parámetros adecuados.
En cambio la parte de mensajes entre Actores sí que requería más trabajo, para no hacer depender un lector de un escritor en exclusiva.

Es cierto que en Java el código es más farragoso y hay controlar más cosas, mi relfexión sin decantarme por que realmente yo mismo me lo pregunto, es si tanta facilidad para "jugar" con procesos simultaneos no puede acabar siendo un problema dado el nivel de la mayoría de programadores que hay por el mundo :).

Gracias por el artículo

Imagen de ezamudio

problema?


Es muy cierto que el problema que tienen muchos programadores con el procesamiento simultáneo o paralelo es conceptual, y eso es independiente del lenguaje que usen, pero la verdad es que en Java sí es algo engorroso el manejo de hilos (ciertamente mucho más sencillo que en C por ejemplo, y creo que cuando hicieron el modelo de Threads en Java la meta era precisamente simplificarlo en comparación con el uso del fork() en C, pero eso fue ya casi dos décadas).

En Scala esconden la parte engorrosa (y muchas veces limitante) de tener que crear un Runnable y ponerle TODO el estado que necesite para poderlo echar a andar en un Thread aparte, y de paso también están ocultando o abstrayendo la parte de sincronización entre hilos. Como lo he visto hasta ahora, un simple:

 

En Java no hay manera más sencilla de implementarla que con algo así:

 

Scala tal vez tras bambalinas está creando ese Runnable por ti, porque para usar una cadena como mensaje, ni siquiera tienes que crear una  , la usas directamente. Y ni hablar del código donde recibes esos eventos...

En fin, creo que ya hoy mismo es necesario que los programadores tengan claros los conceptos de procesamiento en paralelo, en esa parte es donde realmente hay que trabajar, pero para pasar del concepto al ejemplo, me parece mejor Scala que Java.