style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

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 act():

class Proceso(socket:java.net.Socket) extends Actor {

  def act() {
    val reader = new BufferedReader(new InputStreamReader(socket.getInputStream))
    val nombre = reader.readLine
    println ("Nos enviaron " + nombre)
    val out = new PrintStream(socket.getOutputStream)
    out.printf("Hola, %s!%n", nombre)
    out.flush
    socket.close
  }
}

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:

while (true) {
  val sock = server.accept
  println("Nueva conexion")
  new Proceso(sock).start
}

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 switch 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:

object Proceso extends Actor { //los singletons obviamente no pueden tener constructores con parámetros

  def act() { //El mismo método act que ya habíamos visto
    loop { //Aquí se define el loop, con esta palabra reservada
      react { //Y esto es para reaccionar ante un mensaje
        //Aquí definimos el único mensaje que nos interesa:
        //Un socket.
        case socket:java.net.Socket =>
          //Cuando nos manden un socket como mensaje, ejecutaremos este código
          val reader = new BufferedReader(new InputStreamReader(socket.getInputStream))
          val nombre = reader.readLine
          println ("Nos enviaron " + nombre)
          val out = new PrintStream(socket.getOutputStream)
          out.printf("Hola, %s!%n", nombre)
          out.flush
          socket.close
          //Al terminar de ejecutarse esto, continuará el loop esperando más mensajes
      }
    }
  }
}

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 act):

Proceso.start
while (true) {
  val sock = server.accept
  println("Nueva conexion")
  Proceso ! sock
}

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 actor ! mensaje, 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:

//Este lector recibe el InputStream y el proceso que lo ha creado
class Lector(input:InputStream, proc:Proceso) extends Actor {

  def act() {
    //Leemos el esto
    val reader = new BufferedReader(new InputStreamReader(input))
    val nombre = reader.readLine
    //Le mandamos como mensaje al socket al otro actor
    proc ! nombre
    //Terminamos (honestamente no estoy seguro de que sea necesario invocar exit porque no estamos en un ciclo esperando eventos)
    exit
  }
}

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

class Proceso(socket:java.net.Socket) extends Actor {

  def act() {
    //Creamos el lector con el inputStream y una referencia
    //al actor que lo crea, y lo echamos a andar
    val lector = new Lector(socket.getInputStream, this)
    lector.start
    //El ciclo para esperar el mensaje
    loop {
      react {
        //El mensaje será el texto leído
        case nombre:String =>
          println ("Nos enviaron " + nombre)
          val out = new PrintStream(socket.getOutputStream)
          out.printf("Hola, %s!%n", nombre)
          out.flush
          socket.close
          //Aquí sí es necesario invocar exit, para salir del ciclo.
          exit
      }
    }
  }
}

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:

case class Saluda(nombre:String, fecha:java.util.Date, socket:Socket)

Al anteponerle case 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:

object Lector extends Actor {

  def act() {
    loop {
      react {
        //El mensaje que nos van a enviar es un socket
        case s:Socket =>
          //Leemos del socket
          val reader = new BufferedReader(new InputStreamReader(s.getInputStream))
          val nombre = reader.readLine
          //Ahora le enviamos un mensaje al Escritor
          //No necesitamos usar "new" porque es una case class
          Escritor ! Saluda(nombre, new java.util.Date, s)
      }
    }
  }
}

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:

object Escritor extends Actor {

  def act() {
    loop {
      react {
        //Aquí recibimos el mensaje de tipo Saluda
        case Saluda(nom,cuando,socket) =>
          //Podemos usar directamente los objetos que trae el mensaje,
          //haciendo referencia a los nombres que le pusimos aquí
          println("Nos enviaron '" + nom + "' a las " + cuando)
          val out = new PrintStream(socket.getOutputStream)
          out.printf("Hola, %s!%n", nom)
          out.flush
          socket.close
          //No invocamos exit porque eso terminaria la ejecucion del actor, sin
      }
    }
  }
}

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:

//En Java podemos importar UNA clase, o todas las del paquete
//En Scala podemos indicar las clases que queremos importar
import java.net.{ServerSocket,Socket}

//Y además podemos definir varias clases en un mismo archivo
//Aquí defino la case class que usamos en el mensaje de Lector a Escritor
case class Saluda(nombre:String, fecha:java.util.Date, socket:Socket)

object Servidor {

  def run(port:Int) = {
    val server = new ServerSocket(port)
    //Tenemos que iniciar ambos actores
    Lector.start
    Escritor.start
    while (true) {
      val sock = server.accept
      println("Nueva conexion")
      Lector ! sock
    }
  }

  def main(args:Array[String]) = {
    val port = if (args.length > 0) args(0).toInt else 9999
    println("Corriendo servidor en puerto " + port)
    run(port)
  }

}

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.

public class Servidor {

  public static void main(String[] args) {
    try {
      ServerSocket server = new ServerSocket(9999);
      while (true) {
        Socket socket = server.accept();
        //Por ahora dejemosle la chamba de los hilos al lector
        Lector.leer(socket);
      }
    } catch (IOException ex) {
      System.out.println("Ha tronado!");
    }
  }
}

public class Lector implements Runnable {

  private final Socket s;
  private Lector(Socket socket) {
    s = socket;
  }
  public void run() {
    try {
      BufferedReader input = new BufferedReader(new InputStreamReader(s.getInputStream()));
      String nombre = input.readLine();
      //Echemosle la bronca de hacer otro hilo al escritor
      Escritor.escribe(nombre, s);
    } catch (IOException ex) {
      //Algo malo ocurrio que hacemos?
    }
  }

  public static void leer(Socket s) {
    new Thread(new Lector(s)).start();
  }

}

public class Escritor {

  //Esto realmente esta mal ademas; este método se ejecutará en el mismo hilo
  //del Lector, no en uno aparte, pero si lo quisiera hacer en un hilo separado
  //pero sincronizado con el Lector se complica demasiado.
  public static void escribe(String nombre, Socket sock) throws IOException {
    PrintStream pout = new PrintStream(sock.getOutputStream());
    pout.printf("Hola, %s!%n", nombre);
    pout.flush();
    sock.close();
  }

}

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.

Comentarios

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 case class 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 (¿?) equals y hashCode , como por ejemplo una función con el nombre de la clase para que puedas prescindir del "new" ejemplo:

case class Simple()
val s = Simple()

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 readLine() ) 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?

para-lelismo
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:

actor ! "hola"

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

public class Hola implements Runnable {
  private final String msg;
  private final Actor actor;
  public Hola(Actor actor, String mensaje) {
    this.actor = actor;
    msg = mensaje;
  }
  public void run() {
    actor.procesa(msg);
  }
}

//mientras tanto, en una clase muy lejana...
new Thread(new Hola(actor, "hola")).start();
//o si tienes un threadPool
threadPool.execute(new Hola(actor, "hola"));

Scala tal vez tras bambalinas está creando ese Runnable por ti, porque para usar una cadena como mensaje, ni siquiera tienes que crear una case class, 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.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">