Celluloid: Concurrencia en Ruby

Profundizando un poco en el Modelo Actor, en el que se basan lenguajes como Scala, Erlang o Reia, pero que también está disponible a través de frameworks para otros muchos lenguajes, como .NET, Java, Ruby, Python, etc.

Antes de comenzar, conviene que definamos un poco lo que es el modelo Actor. Según la wikipedia (versión inglesa) el modelo Actor es: un modelo matemático de computación concurrente que trata a los “actores” como las primitivas universales de la computación concurrente digital: en respuesta a mensajes que recibe, un actor puede tomar decisiones locales, crear más actores, enviar más mensajes y determinar cuando responder al siguiente mensaje recibido.

En este artículo me gustaría centrarme en Celluloid, un framework del modelo Actor para Ruby. Este framework nos permite tratar las instancias de los objetos como procesos autónomos, concurrentes, que atienden peticiones y mantienen su memoria, sin posibilidad de que pueda ser corrompida por dos mensajes simultáneos.

Un ejemplo de código de la propia página web, haciendo alusión al actor… vamos a castellanizarlo un poco:

class Bardem
  include Celluloid::Actor
 
  def initialize(nombre)
    @nombre = nombre
  end
 
  def pon_estado(estado)
    @estado = estado
  end
 
  def informa
    "#{@nombre} está #{@estado}"
  end
end

Para poder probarlo necesitamos de un entorno de pruebas que nos permita la ejecución de este código. Yo suelo usar rvm para poder instalar entornos de ruby de forma cómoda y poder probarlos, por lo que, solo hay que poner en consola:

rvm install 1.9.2-celluloid
rvm use 1.9.2-celluloid
gem install celluloid

IMPORTANTE: tener presente que Celluloid solo funciona en JRuby en modo 1.9, en Rubinius y en Ruby a partir de la versión 1.9.2, ya que las anteriores no tienen hilos de estado concurrente real.

En una consola de irb, se podría ejecutar lo siguiente:

ruby-1.9.2-p180 :001 > require "celluloid"
 => true 
ruby-1.9.2-p180 :002 > require "./bardem.rb"
 => true 
ruby-1.9.2-p180 :003 > javier = Bardem.spawn "Javier Bardem"
 => #<Celluloid::Actor(Bardem:0x7959dc) @nombre="Javier Bardem"> 
ruby-1.9.2-p180 :004 > javier.pon_estado "ha ganado!"
 => "ha ganado!" 
ruby-1.9.2-p180 :005 > javier.informa
 => "Javier Bardem ha ganado!" 
ruby-1.9.2-p180 :006 > javier.pon_estado! "gana asincronamente!"
 => nil 
ruby-1.9.2-p180 :007 > javier.informa
 => "Javier Bardem gana asincronamente!" 

Cuando se llama a spawn se crea el objeto dentro de su propio hilo. El paso de mensajes se realiza usando el manejador del actor específico (javier en este caso) y se puede pasar un mensaje de forma síncrona (esperando su resultado) o de forma asíncrona (con la exclamación) sin esperar a que retorne nada.

Supervisores

Los supervisores, son procesos que se encargan de supervisar que un proceso está levantado y funcionando (como los monitores), pudiendo reiniciarlos cada vez que se caen. Por ejemplo, podemos crear un supervisor del ejemplo anterior de la siguiente forma:

ruby-1.9.2-p180 :008 > penelope = Bardem.supervise "Javier Bardem"
 => #<Celluloid::Supervisor(Bardem) "Javier Bardem"> 
ruby-1.9.2-p180 :009 > javier = penelope.actor
 => #<Celluloid::Actor(Bardem:0x73bc20) @nombre="Javier Bardem"> 

Así, creamos un supervisor (penelope) que supervisa al actor Javier Bardem. También podemos asignar un nombre al actor, de modo que:

ruby-1.9.2-p180 :011 > Bardem.supervise_as :javier, "Javier Bardem"
 => #<Celluloid::Supervisor(Bardem) "Javier Bardem"> 
ruby-1.9.2-p180 :012 > javier = Celluloid::Actor[:javier]
 => #<Celluloid::Actor(Bardem:0x726118) @nombre="Javier Bardem"> 

De modo que el símbolo :javier se relacione con el proceso y pueda emplearse en lugar de Javier Bardem, cuando se realice una llamada.

Enlazado

Si se sucediese una excepción no manejada en cualquiera de los métodos del actor, ese actor se caería y moriría. Vamos a poner un ejemplo:

class JamesDean
  include Celluloid::Actor
  class CarInMyLaneError < StandardError; end
 
  def drive_little_bastard
    raise CarInMyLaneError, "that guy's gotta stop. he'll see us"
  end
end

Ahora, vamos a poner a James en el coche Little Bastard conduciendo y veamos que sucede:

ruby-1.9.2-p180 :001 > require "celluloid"
 => true 
ruby-1.9.2-p180 :002 > require "./james_dean.rb"
 => true 
ruby-1.9.2-p180 :003 > james = JamesDean.spawn
 => #<Celluloid::Actor(JamesDean:0x8ec024)> 
ruby-1.9.2-p180 :004 > james.drive_little_bastard!
 => nil 
E, [2011-10-05T17:54:33.582990 #3929] ERROR -- : JamesDean crashed!
JamesDean::CarInMyLaneError: that guy's gotta stop. he'll see us
/home/marubio/tmp/james_dean.rb:6:in `drive_little_bastard'
[...]
ruby-1.9.2-p180 :005 > james
 => #<Celluloid::Actor(JamesDean:0x8ec024) dead> 

Cuando le dijimos a james de forma asíncrona que condujese Little Bastard, ¡lo mató! Si fuésemos Elizabeth Taylor, co-protagonista de la última película al tiempo de su muerte, querríamos saber cuando murió. ¿Cómo podemos hacer eso? Enlazando a los actores que estén interesados en las caídas de otros actores. Para recibir estos eventos, necesitamos usar el método trap_exit. Un ejemplo:

class ElizabethTaylor
  include Celluloid::Actor
  trap_exit :actor_died
 
  def actor_died(actor, reason)
    puts "Oh no! #{actor.inspect} ha muerto por #{reason.class}"
  end
end

Por lo que al ejecutar lo siguiente, obtenemos:

ruby-1.9.2-p180 :001 > require "celluloid"
 => true 
ruby-1.9.2-p180 :002 > require "./james_dean.rb"
 => true 
ruby-1.9.2-p180 :003 > require "./elizabeth_taylor.rb"
 => true 
ruby-1.9.2-p180 :004 > james = JamesDean.spawn
 => #<Celluloid::Actor(JamesDean:0xcaa38c)> 
ruby-1.9.2-p180 :005 > elizabeth = ElizabethTaylor.spawn
 => #<Celluloid::Actor(ElizabethTaylor:0xca43b0)> 
ruby-1.9.2-p180 :006 > elizabeth.link james
 => #<Celluloid::Actor(JamesDean:0xcaa38c)> 
ruby-1.9.2-p180 :007 > james.drive_little_bastard!
 => nil 
E, [2011-10-06T11:26:31.659461 #8552] ERROR -- : JamesDean crashed!
JamesDean::CarInMyLaneError: that guy's gotta stop. he'll see us
ruby-1.9.2-p180 :008 > Oh no! #<Celluloid::Actor(JamesDean:0xcaa38c) dead> ha muerto por JamesDean::CarInMyLaneError

Como vemos, Elizabeth es notificada inmediatamente a través del trap, permitiéndole reaccionar ante la caída de James. Pero también podríamos querer lanzar un objeto y enlazarlo al mismo tiempo.

Conclusiones

El resto del tutorial (en inglés) se encuentra en la página oficial de Celluloid, donde se mencionan otras formas de enlazado, registro y logs. Yo me quedo en este punto, porque creo que ha sido suficiente para adentar la programación de Modelo Actor al lenguaje Ruby. Espero que os sirva para resolver algún que otro problema de programación en el que usualmente se empleen semáforos, monitores o memoria compartida.