Follow me
RSS feed
My sources
My Viadeo

Création d'un modèle d'acteur en Ruby

Greg | 18 Jun 2011

Projets Depuis 2 sprints, nous travaillons, @syl20j, @jblemee et moi, sur un nouveau projet chez VIDAL. Partants de rien, nous avons choisi de développer en Scala, ce qui nous a ouvert la voie vers l'utilisation du modèle d'acteur. Mes divertissements avec Erlang m'avaient déjà permis de jouer un peu avec. Pourtant, il aura fallu que je l'utilise dans un contexte professionnel pour véritablement y consacrer du temps et essayer de l'appliquer en Ruby.

Les acteurs

Le principe du modèle d'acteur vise avant tout à simplifier le traitement parallèle. Pour cela il définit les règles suivantes :

Avant de voir comment nous pouvons mettre en place un modèle d'acteur en Ruby, il peut être intéressant de regarder comment il s'utilise dans d'autres langages.

Erlang

Les acteurs sont classés dans le chapitre Processes d'Erlang. Ils sont très simples à mettre en place. Il suffit de créer une méthode définissant receive :

actor.erl
-module(actor).
-export([myActor/0]).

myActor() ->
   receive  
   {mult, X} ->
      io:format(standard_io, "~w * ~w = ~w~n", [X, X, X*X]),
      myActor();
   {add, X} ->
      io:format(standard_io, "~w + ~w = ~w~n", [X, X, X+X]),
      myActor();
   Other ->
      io:format(standard_io, "Don't know what to do with message : ~w~n", [Other]),
      myActor()
end.

Dans cet exemple, notre acteur peut recevoir deux types de messages :

Tout autre message (Other) affichera une réponse du type : Don't know what to do with message : ~w~n.

Pour utiliser cet acteur, nous allons créer un processus en utilisant la méthode spawn. Nous pourrons ensuite envoyer des messages à notre acteur en utilisant l'opérateur ! :

Erlang R14B03 (erts-5.8.4) [source] [64-bit] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8.4  (abort with ^G)
1> c(actor).
{ok,actor}
2> Pid = spawn(fun actor:myActor/0).
<0.38.0>
{mutl,4}
3> Pid ! {mult, 4}.
4 * 4 = 16
{mult,4}
4> Pid ! {add, 4}. 
4 + 4 = 8
{add,4}
5> Pid ! {something, 4}.
Don't know what to do with message : {something, 4}

Scala

Comme pour Erlang, les acteurs existent nativement en Scala. Pour créer un acteur, il suffit de créer une classe étendant la classe scala.actors.Actor. Il faut définir dans cette classe la méthode act qui sera appelé lors de la réception d'un message. Pour récupérer le contenu du message, nous utilisons la méthode receive :

import scala.actors.Actor

case class Mult(value: Int)
case class Add(value: Int)
case class Something(value: Int)
case object Stop

class MyActor extends Actor {
   def act() {
      loop {
         receive {
            case Mult(value) => println(value + " * " + value + " = " + value * value)
            case Add(value) => println(value + " + " + value + " = " + (value + value))
            case Stop => exit()
            case msg => println("Don't know what to do with message :" + msg)
         }
      }
   }
}

Dans cet exemple nous avons utilisé des case class et case object comme messages. Pour envoyer des messages à notre acteur, nous devons avant tout l'initialiser et le démarrer (start). Nous utilisons ensuite ! pour envoyer un message à l'acteur.

val myActor = new MyActor()
myActor.start

myActor ! Mult(4)
myActor ! Add(4)
myActor ! Something(4)

myActor ! Stop

Sachez également qu'il est possible d'envoyer un message à un acteur scala en utilisant !! ou !?. Dans le premier cas, nous recevrons en retour un objet Futur pour récupérer une réponse asynchrone. Avec !? nous attendons que l'acteur ait terminé sont traitement pour récupérer la réponse ; c'est donc un appel synchrone.

Java

Il n'existe rien de natif en Java pour utiliser un modèle d'acteur. Si vous souhaitez le faire, je vous conseille de regarder du côté d'akka. Ce framework d'acteur, écrit initialement pour scala permet d'implémenter très facilement des acteurs en Java. Pour cela il suffit de créer une classe étendant la classe akka.actor.Actor et de lui définir une méthode public void onReceive(Object message) qui recevra les messages. Par la suite, on utilisera Actors.actorOf pour créer l'acteur. On le démarrera avec start et on lui enverra des messages via sendOneWay ou sendRequestReplyFuture.

Il existe de nombreux exemples d'utilisation d'akka avec Java.

Ruby

En 2008, Tony Arcieri annonçait sur la mailling liste Ruby la sortie de Revactor. Depuis, le projet semble tombé aux oubliettes, mais le même homme nous a gratifiés de celluloid, une solution inspirée d'Erlang. Il existe également le projet girl_friday qui mérite d'être regardé...

Pourquoi donc développer une nouvelle solution ?

Avant tout pour mieux comprendre le fonctionnement du modèle d'acteur. Mais aussi pour s'amuser ;)

Commençons par mettre en place un objectif. Pour cela, je partirai sur le modèle d'akka que je trouve particulièrement bien pensé. Nous allons donc écrire un petit exemple qui devra fonctionner avec nos acteurs :

class MyActor < Actor
  def receive(message)
    action = message[0]
    value = message[1]
    case action
      when :mult then
        puts "#{value} * #{value} = #{value * value}"
      when :add then
        puts "#{value} + #{value} = #{value + value}"
      else 
        puts "Don't know what to do with message : #{message}"
    end 
  end 
end

myActor = Actors.actor_of(MyActor).start

myActor | [:mult, 4]
myActor | [:add, 4]
myActor | [:something, 4]

Maintenant que nous savons à quoi nous voulons aboutir, lançons-nous.

Nous avons besoin de deux classes Actor et Actors. La première nous servira à créer nos acteurs, la seconde comprenant des méthodes pour l'utilisation de ces acteurs.

Pour créer ces classes, nous avons quelques défis à relever. Le premier concerne la possibilité de communiquer entre des threads. En effet, nos acteurs travaillants dans des threads différents, nous avons besoin d'un mécanisme permettant de passer un message de l'un à l'autre, ces messages devant être traités séquentiellement. Nous pouvons régler ce problème très simplement en utilisant la classe Queue de Ruby :

require 'thread'

queue = Queue.new

Thread.new do 
  loop { puts "Message receive by #{Thread.current} : #{queue.pop}" }
end

queue << "Hello World!"
queue << "Bonjour le monde !"
queue << "Hola mundo!"

sleep 2

Nous pouvons donc créer une classe SimpleActor :

class SimpleActor
  def initialize #:nodoc:
    @actorQueue = Queue.new
  end 

  # Start actor and return it
  def start
    @thread = Thread.new do  
      loop do
        receive(@actorQueue.pop)
      end 
    end 

    return self
  end 

  # Stop actor
  def stop
    @thread.kill
  end

  # Send a simple message without expecting any response
  def |(message)
    if @thread.alive?
      @actorQueue << message
    else
      raise "Actor is dead!"
    end
  end

  private
  # Method receiver for the actor
  def receive(message)
    raise "Define me!"
  end
end

Avec cette classe, nous sommes capables de créer des acteurs simples et de leur envoyer des messages.

class MyActor < SimpleActor
  def receive(m)
    ...
  end
end

myActor = MyActor.new
myActor | "Hello World!"

Il faut maintenant ajouter la possibilité de recevoir une réponse de l'acteur. Dans le cas d'une réponse synchrone, sachant que Queue#pop attend qu'il y ait des données dans la queue, nous pouvons utiliser une seconde queue dans laquelle l'acteur placera sa réponse. Pour mettre cela en place, nous allons créer la classe Actor capable de gérer les réponses de l'acteur, et héritant de SimpleActor :

class Actor < SimpleActor
  def initialize
    super
    @main = Queue.new
  end

  # Send a synchronous message
  def <<(message)
    if @thread.alive?
      @actorQueue << message
      @main.pop
    else
      raise "Actor is dead!"
    end
  end

  private
  # Method use by the actor to reply to his sender
  def reply( r )
    @main << r
  end
end

Nous pouvons maintenant envoyer un message et attendre une réponse synchrone, cette réponse étant renvoyé par l'acteur en utilisant Actor#reply :

class MyActor < Actor
  def receive(m)
    ...
    reply(...)
  end
end

myActor = MyActor.new
response = myActor << "Hello World!" 

Pour la gestion des réponses asynchrones de la part de l'acteur, nous avons plusieurs possibilités. La plus évidente consiste à utiliser un bloc. Le problème de cette solution est que si nous voulons utiliser un opérateur (< dans mon cas), la notation devient moche. En effet l'écriture suivante n'est pas possible :

myActor < message { |response|
  ...
}

Pour que cela fonctionne, il faut utiliser la notation pointée :

myActor.<(message) { |response|
  ...
}

Je vous avais prévenu... C'est moche...

Une autre solution consiste à créer une méthode actor_response dans le thread appelant, et faire en sorte que la réponse de l'acteur soit envoyée à cette méthode. Nous nous retrouvons alors avec un nouveau problème : comment récupérer l'objet appelant dans une méthode ? Pour répondre à cela, nous allons utiliser le projet binding_of_caller de James M. Lawrence. Pour récupérer l'objet appelant d'une méthode nous utiliserons le code suivant :

class MaClass
  def ma_methode
    BindingOfCaller.binding_of_caller do |bind|
      self_of_caller = eval("self", bind)
    end
  end
end

Nous pourrons alors utiliser la méthode Object#send pour appeler des méthodes de l'objet appelant.

Nous pouvons donc très facilement créer notre méthode d'envoi de message asynchrone :

def <(message)
  if @thread.alive?
    @actorQueue << message
    BindingOfCaller.binding_of_caller do |bind|
      self_of_caller = eval("self", bind)
      Thread.new do
        _result = @main.pop
        self_of_caller.send( :actor_response, _result )
      end
    end
  else
    raise "Actor is dead!"
  end
end

Pour l'envoi d'un message nous pouvons donc utiliser < et il suffit de définir la méthode actor_response pour récupérer la réponse :

def actor_response(response)
  puts "Actor response = #{response}"
end

myActor << message

Il ne nous reste plus qu'à mettre en place la classe Actors définissant la méthode actor_of :

class Actors
  # Create a new Actor with class klass
  def self.actor_of(klass)
    return klass.new()
  end
end

L'intérêt de cette classe est relativement faible. Elle permet uniquement de mimer la méthode actorOf d'akka.

Notre solution est maintenant complète. Nous pouvons le vérifier en exécutant le code que nous avons mis en place lorsque nous avons fixé notre objectif :

4 * 4 = 16
4 + 4 = 8
Don't know what to do with message : [:something, 4]

L'ensemble du code a été déposé sur github. N'hésitez pas à forker, modifier, corriger...

Copyright © 2009 - 2011 Grégoire Lejeune.
All documents licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License, except ones with specified licence.
Powered by Jekyll.