Création d'un modèle d'acteur en Ruby
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 :
- Un acteur participe à un traitement parallèle et ne partage rien avec les autres acteurs.
- On communique avec un acteur uniquement par le biais de messages.
- Les messages reçus par un même acteur sont traités séquentiellement (et non en parallèle) en suivant la règle FIFO.
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 :
- {mult, X} : dans ce cas, l'acteur affichera le résultat de la multiplication de X par X.
- {add, X} : dans ce cas, l'acteur affichera le résultat de la somme de 2 X.
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...