Nous allons détailler le processus de développement dans un 1erexemple ; puis, les exemples suivants serviront chacun à expliquer la mise en
uvre de certaines caractéristiques de JAVACT.
Nous utilisons trois acteurs qui s'exécutent sur trois places. Le premier se chargera d'imprimer « Hello », puis activera le second qui affichera alors « Wonderful » et activera le troisième, qui lui affichera « World ».
La fig.1 présente l'articulation entre les diverses interfaces à définir. Chaque comportement doit être spécifié par une interface appropriée qui doit hériter de BehaviorProfile, et déclarer:
import javact.util.BehaviorProfile;
import javact.util.ActorProfile;
import javact.lang.Actor;
interface Hello extends BehaviorProfile, ActorProfile {
/* Paramètres : les acteurs chargés d'afficher "Wonderful" et "World" */
public void print(Actor wonderful, Actor world);
}
import javact.util.BehaviorProfile;
import javact.util.ActorProfile;
import javact.lang.Actor;
interface Wonderful extends BehaviorProfile, ActorProfile {
/* Paramètre : l'acteur chargé d'afficher "World" */
public void print(Actor world);
}
import javact.util.BehaviorProfile;
import javact.util.ActorProfile;
interface World extends BehaviorProfile, ActorProfile {
public void print();
}
En fait, on peut spécifier deux sortes de méthodes auxquelles correspondent deux sortes de messages :
Les messages avec retour sont des messages spéciaux, qui restent asynchrones et non bloquants pour l'émetteur mais qui lui permettent d'obtenir une réponse. Quand il en a besoin, l'émetteur doit la demander explicitement en faisant un appel à la méthode getReply() de l'objet message ; alors, il se bloque jusqu'à son obtention. Auparavant il aura pu effectuer d'autres actions.
Pour les messages asynchrones classiques, le préfixe JAM est ajouté devant le nom du message pour obtenir le nom du fichier java correspondant. Pour les messages avec retour, on ajoute le préfixe JSM ainsi que le nom du type de retour, pour distinguer les messages de même nom mais renvoyant des valeurs de types différents12, et la méthode getReply() est engendrée avec le « bon » type de retour.
).
Pour effectuer la génération automatique, il suffit d'exécuter le script javactgen (dans le sous-répertoire bin) qui se chargera d'extraire toutes les informations nécessaires à la génération. Cela donnera un résultat semblable au suivant:
$ javactgen *.java
Found the following actor profile(s): Hello, Wonderful, World.
Compiling files defining actor profile interfaces...
Extracting messages...
Handling the actor profile Hello...
This is also a behavior profile.
Handling the actor profile Wonderful...
This is also a behavior profile.
Handling the actor profile World...
This is also a behavior profile.
Generating QuasiBehaviors...
Generating messages...
Compiling generated files...
Pour Hello World les fichiers générés seront:
import javact.lang.Actor;
class HelloBeh extends HelloQuasiBehavior {
public void print(Actor wonderful, Actor world) {
System.out.println("<" + myPlace() + "> -- Hello ...");
send(new JAMprint(world), wonderful);
suicide();
}
}
Lorsqu'il traite le message print, l'acteur affiche « Hello »
puis transmet le message aux acteurs suivants. Enfin il se suicide
puisqu'il a terminé sa tâche.
Nous obtenons finalement la structure présentée en fig.2.
import javact.lang.Actor;
import javact.net.rmi.CreateCt;
import javact.net.rmi.SendCt;
public class HelloWorld {
public static void main(String[] args) {
if (args.length == 3) {
Actor hello = CreateCt.STD.create(args[0], new HelloBeh());
Actor wonderful = CreateCt.STD.create(args[1], new WonderfulBeh());
Actor world = CreateCt.STD.create(args[2], new WorldBeh());
SendCt.STD.send(new JAMprint(wonderful, world), hello);
} else {
System.out.println("Running on randomly determinated places");
Actor hello = CreateCt.STD.create(new HelloBeh());
Actor wonderful = CreateCt.STD.create(new WonderfulBeh());
Actor world = CreateCt.STD.create(new WorldBeh());
SendCt.STD.send(new JAMprint(wonderful, world), hello);
}
}
}
Les trois acteurs sont créés sur trois places dont les noms (String) sont passés en ligne de commande (ou, par défaut, choisies aléatoirement au sein du domaine défini dans le fichier places.txt), puis le premier est activé en lui envoyant
un message « print ». C'est ce message qui permet d'amorcer la communication.
La compilation des classes restantes se fait avec le script javactc
(sous-répertoire bin). Il s'utilise exactement comme le
compilateur javac et doit recevoir en paramètre le fichier contenant
la déclaration « public static void main... ».
Pour l'exécution de cet exemple ou pourra se reporter à la section précédente.
Nous allons ici illustrer le changement de comportement avec une cellule alternativement vide et pleine14. Les profils de comportement sont les suivants:
interface Full extends BehaviorProfile {
public void get(Actor a);
public void become(Empty bp);
}
interface Empty extends BehaviorProfile {
public void put(int v);
public void become(Full bp);
}
L'acteur correspondant est décrit par le profil suivant :
interface Cell extends Empty, Full, ActorProfile {
}
Pour illustrer le fonctionnement de la cellule nous définissons un client qui récupèrera son contenu:
interface Client extends BehaviorProfile, ActorProfile {
public void val(int v) ;
}
L'annexe B
p.
donne un exemple de comportement
automatiquement généré.
L'implantation des comportements sera la suivante:
class EmptyCellBeh extends EmptyQuasiBehavior {
public void put(int i) {
System.out.println("Empty cell " + ego() + " receives put(" + i + ") request");
become(new FullCellBeh(i, this));
}
}
class FullCellBeh extends FullQuasiBehavior {
int i;
Empty empty;
FullCellBeh(int i, Empty empty) {
this.i = i;
this.empty = empty;
}
public void get(Actor a) {
System.out.println("Full cell " + ego() +
" receives get request; will return " + i);
send(new JAMvalue(i), a);
become(empty);
}
}
class ClientBeh extends ClientBehavior {
public void value(int i) {
System.out.println("Client " + ego() + " receives " + i);
}
}
Et le programme principal pourra être le suivant :
public static void main(String[] args) {
Actor cell = CreateCt.STD.create("localhost", new EmptyCellBeh());
SendCt.STD.send(new JAMput(2004), cell);
Actor customer = CreateCt.STD.create("localhost:1100", new ClientBeh());
SendCt.STD.send(new JAMget(customer), cell);
}
Ici, la cellule et le client sont respectivement créés sur les places localhost et localhost:1100.
L'exemple présenté dans cette partie concerne le calcul des N premiers nombres premiers par la méthode du Crible d'Eratosthène. Cet exemple illustre en particulier la faculté de création dynamique des acteurs. L'algorithme est proche d'une version parallèle initialement proposée par D. Gries15.
Le calcul repose sur la construction dynamique d'un pipeline d'acteurs, chacun représentant un nombre premier, les acteurs étant chaînés dans l'ordre croissant. Chacun des acteurs joue le rôle d'un crible plus sélectif que le crible précédent. Tout acteur est conçu pour recevoir et traiter (méthode sift) un message contenant un nombre à cribler (message JAMsift). En terme de communication, tous les acteurs du crible offrent donc la même interface.
Tous les messages sont envoyés au premier acteur du pipeline, un par un et dans l'ordre croissant. Lorsque le nombre reçu est un multiple du nombre (premier) que l'acteur représente, il ne traverse pas le crible et il est abandonné. En revanche, lorsque ce nombre n'est pas multiple, deux cas se présentent, selon que l'acteur qui le traite est à l'extrémité du pipeline ou pas. Chacun de ces cas est implanté par un comportement différent (classe LastBeh pour le comportement de l'acteur extrémité, classe IntermediateBeh dans l'autre cas) :
Quand un acteur du pipeline a pris un comportement d'acteur intermédiaire, il le conserve définivement. Ceci conduit à spécifier deux interfaces de comportement qui diffèrent par l'existence de la méthode become16. Alors, on peut spécifier les profils ainsi :
public interface IntermediateSieve extends BehaviorProfile {
public void sift(int i);
}
public interface LastSieve extends IntermediateSieve {
public void become(IntermediateSieve b);
}
public interface Sieve extends IntermediateSieve, LastSieve, ActorProfile {
}
Les comportements peuvent alors s'implémenter comme suit :
class IntermediateBeh extends IntermediateSieveQuasiBehavior {
protected int prime;
protected Actor next;
public IntermediateBeh(int i, Actor a) {
super();
prime = i;
next = a;
}
public void sift(int i) {
if ((i % prime) != 0)
send(new JAMsift(i), next);
}
}
class LastBeh extends LastSieveQuasiBehavior {
protected int prime;
public LastBeh(int i) {
super();
prime = i;
System.out.println("*** " + prime + " is a prime number ***");
}
public void sift(int i) {
if ((i % prime) != 0) {
Actor next = create(new LastBeh(i));
become(new IntermediateBeh(prime, next));
}
}
}
L'application commence par la création de l'acteur pour l'entier 2 auquel sont envoyés les nombres dans l'ordre croissant17.
public class PrimeNumbers {
public static void main(String[] args) {
if (args.length != 0) {
int valint = Integer.parseInt(args[0]);
if (valint > 2) {
Actor two = CreateCt.STD.create(new LastBeh(2));
for (int i = 3; valint >= i; i++)
SendCt.STD.send(new JAMsift(i), two);
} else System.out.println("Give an int > 2");
}
}
}
Les deux captures d'écran (figures 3 et 4) montrent la trace de l'exécution (ici, les acteurs sont créés aléatoirement sur une des places du domaine, ce qui en l'occurrence engendre une inefficacité certaine). Dans la figure 4, on voit en bas à gauche la fenêtre montrant caraibe station Sun sous SunOS, en haut à droite la ligne de commande du crible (notez l'utilisation d'un serveur de classe sur chambord:8080), et en bas à droite le listing des classes demandées au serveur de classe
Nous considérons maintenant une application dans laquelle un agent mobile recherche des informations sur des places qu'il visite18. Par cet exemple, nous illustrons d'une part la mobilité d'agent et ses différents aspects, d'autre part le mécanisme d'envoi de messages avec retour de résultats.
Ici, pour l'exemple, l'information recherchée est le volume de mémoire disponible. L'agent mobile de recherche est susceptible de recevoir et de traiter trois types de requêtes :
Selon le type de demande, l'agent opère comme suit :
Pour l'agent mobile, de type browser, les méthodes browseAndJump() et getInfos() définissent l'ensemble des messages qu'il peut traiter. Le traitement d'une demande de recherche complexe impose à l'agent de prendre un comportement particulier dans lequel il va parcourir l'itinéraire et rechercher l'information, sans traiter les messages en attente.
Toutes les requêtes étant envoyées à l'agent mobile par le programme principal, nous ne spécifions pas d'agent client. En revanche, l'application implique un agent superviseur (dont la référence est connue de l'agent mobile) et qui a pour seul rôle d'être informé par message lorsqu'un déplacement de l'agent échoue (méthode trace()).
La spécification des profils des comportements et des acteurs conduit donc aux définitions ci-dessous :
public interface OneStepBrowser extends BehaviorProfile {
public void browseAndJump(String place);
public void browseAndJump(Vector itinerary, String place);
public Vector getInfos();
void become(MultiStepBrowser b);
}
public interface MultiStepBrowser extends BehaviorProfile {
void become(OneStepBrowser b);
}
public interface Browser extends OneStepBrowser, MultiStepBrowser, ActorProfile {
}
public interface Supervisor extends BehaviorProfile, ActorProfile {
public void trace(String s);
}
Le prétraitement de ces interfaces (via le script javactgen) permet d'engendrer les classes :
La classe JSMgetInfosVector offre une méthode getReply() qui permet la récupération du résultat de la requête :
public class JSMgetInfosVector extends MessageWithReply {
private java.util.Vector result ;
...
public java.util.Vector getReply() {
...
}
}
Pour implanter les comportements, on dispose de la méthode go(String p) qui permet d'indiquer que le prochain message sera traité (avec le prochain comportement) sur la place identifiée par p. Comme c'est le cas pour le changement de comportement, le déplacement n'est pas réellement effectué au moment de l'appel à go() mais avant de traiter le message suivant19. Précisons que :
En revanche, via un mécanisme d'exception interne à la bibliothèque, il est possible pour le programmeur, de contrôler un échec éventuel lors du déplacement : la méthode go(String p, HookInterface h) permet d'associer au déplacement un objet dédié à la reprise du calcul en cas d'echec au déplacement, et dont la méthode void resume() sera alors automatiquement invoquée. On peut programmer la reprise du calcul au sein d'une classe qui implante l'interface HookInterface en implantant la méthode resume() ; si cette classe est interne à la classe comportement, on peut accéder à tous les éléments du comportement courant, et en particulier redéfinir le déplacement et le comportement suivant de l'agent.
Pour que l'agent puisse opérer de manière autonome sur son itinéraire, le comportement de recherche complexe doit implanter l'interface javact.util.StandAlone : cette interface spécifie une méthode run() qui est exécutée automatiquement à chaque fois qu'un agent est installé (par création ou déplacement) sur une place. Via cette interface, on peut d'une part créer des agents auto-actifs sans qu'il soit nécéssaire de les activer par message, d'autre part (c'est ici le cas) des agents capables de se déplacer de manière autonome sur le réseau 20.
On code alors les comportements comme suit (la structure de donnée est déclarée Serializable afin de pouvoir être déplacée et transmise sans provoquer une exception) :
class Info implements Serializable {
private long freeMemory;
private String place;
public Info(String place, long freeMemory) {
this.place = place;
this.freeMemory = freeMemory;
}
long getFreeMemory() {
return freeMemory;
}
String getPlace() {
return place;
}
public String toString() {
return place + " : " + freeMemory;
}
}
class SimpleHook implements HookInterface {
Actor supervisor;
SimpleHook(Actor supervisor) {
this.supervisor = supervisor;
}
public void resume(GoException e) {
// Nothing to do : the agent stays in its current place. A message is sent.
SendCt.STD.send(new JAMtrace("Unreachable place : " + e.getPlace()), supervisor);
}
}
/**
* The behavior for (when activated) local browsing, then moving.
*/
class OneStepBrowserBeh extends OneStepBrowserQuasiBehavior {
Actor supervisor;
Vector collectedInfos;
class Hook implements HookInterface {
Vector itinerary;
String place;
Hook(Vector itinerary, String place){
this.itinerary = itinerary;
this.place = place;
}
public void resume(GoException e) {
String message = ego() + " : " + e.getPlace() + " is unreachable from "
+ myPlace() + " - Moves to " + itinerary.firstElement();
send(new JAMtrace(message), supervisor);
go((String) itinerary.firstElement(), new Hook(itinerary, place));
itinerary.removeElementAt(0);
become(new MultiStepBrowserBeh(supervisor, collectedInfos, itinerary, place));
}
}
// Constructors
OneStepBrowserBeh(Actor supervisor) {
this.supervisor = supervisor;
this.collectedInfos = new Vector();
}
OneStepBrowserBeh(Actor supervisor, Vector collectedInfos) {
this.supervisor = supervisor;
this.collectedInfos = collectedInfos;
}
// Methods
public void browseAndJump(String place) {
// local browsing first
collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory()));
// then, moving
go(place, new SimpleHook(supervisor));
}
public void browseAndJump(Vector itinerary, String place) {
// local browsing first
collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory()));
if (!itinerary.isEmpty()) {
// then travelling
go((String) itinerary.firstElement(), new Hook(itinerary, place));
itinerary.removeElementAt(0);
become(new MultiStepBrowserBeh(supervisor, collectedInfos, itinerary, place));
} else
// else moving
go(place, new SimpleHook(supervisor));
}
public Vector getInfos() {
Vector saveCollectedInfos = collectedInfos;
collectedInfos = new Vector();
return saveCollectedInfos;
}
}
/**
* The behavior for automatic browsing and moving on a list of places
*/
class MultiStepBrowserBeh extends MultiStepBrowserQuasiBehavior implements StandAlone {
Actor supervisor;
Vector collectedInfos;
Vector itinerary;
String place;
class Hook implements HookInterface {
Vector itinerary;
String place;
Hook(Vector itinerary, String place){
this.itinerary = itinerary;
this.place = place;
}
public void resume(GoException e) {
String message = ego() + " : " + e.getPlace() + " is unreachable from "
+ myPlace() + " - Moves to ";
if (itinerary.size()>0) {
message+=itinerary.firstElement();
send(new JAMtrace(message), supervisor);
go((String) itinerary.firstElement(), new Hook(itinerary, place));
itinerary.removeElementAt(0);
} else {
message+=place;
send(new JAMtrace(message), supervisor);
go(place);
}
}
}
// Constructor
MultiStepBrowserBeh(Actor supervisor, Vector collectedInfos,
Vector itinerary, String place) {
this.collectedInfos = collectedInfos;
this.itinerary = itinerary;
this.place = place;
}
// Method
public void run() {
// local browsing first
collectedInfos.addElement(new Info(myPlace(), Runtime.getRuntime().freeMemory()));
if (!itinerary.isEmpty()) {
// then travelling
go((String) itinerary.firstElement(), new Hook(itinerary, place));
itinerary.removeElementAt(0);
} else {
// else moving
become(new OneStepBrowserBeh(supervisor, collectedInfos));
go(place, new SimpleHook(supervisor));
}
}
}
class SupervisorBeh extends SupervisorQuasiBehavior {
public void trace(String s) {
System.out.println(s);
}
}
Enfin, on écrit le programme principal. Rappelons que l'envoi de messages avec retour de résultat n'est pas bloquant pour l'émetteur de la requête : ici, l'émetteur n'est pas bloqué pendant que l'agent parcours son itinéraire, mais seulement au moment où il demande explicitement le résultat en attendant son arrivée.
public class MobileBrowser {
public static void main(String[] args) {
if (args.length >= 2) {
Actor supervisor = CreateCt.STD.create(new SupervisorBeh());
Actor browser = CreateCt.STD.create(args[0], new OneStepBrowserBeh(supervisor));
for (int i = 1; i < args.length; i++)
SendCt.STD.send(new JAMbrowseAndJump(args[i]), browser);
SendCt.STD.send(new JAMbrowseAndJump(args[0]), browser);
JSMgetInfosVector m1 = new JSMgetInfosVector();
SendCt.STD.send(m1, browser);
Vector itinerary = new Vector();
for (int i = 1; i < args.length; i++) itinerary.addElement(args[i]);
SendCt.STD.send(new JAMbrowseAndJump(itinerary, args[0]), browser);
JSMgetInfosVector m2 = new JSMgetInfosVector();
SendCt.STD.send(m2, browser);
System.out.println(m1.getReply());
System.out.println(m2.getReply());
}
}
}
La capture d'écran (figure 5) montre l'exécution de cet exemple, avec deux machines caraibe et chambord actives. La place XXX n'existe pas, et la trace d'exécution montrée par le superviseur (fenêtre de gauche) indique les actions de reprise sur erreur. La fenêtre de lancement (en haut à droite) contient également les lignes de commande pour la génération des classes intermédiaires ainsi que la compilation du paquetage MobileBrowser.