MokaByte Numero 16 - Febbraio 1998
 
 

 

La programmazione 
multiagente
di
Massimiliano
Baldinelli
Una breve rassegna di alcune librerie che realizzano un nuovo paradigma per la programmazione distribuita: Aglets e JAFMAS. La programmazione ad agenti, evoluzione della OOP

 


1.Introduzione
La grande novità degli anni 90 nel campo dello sviluppo software è stata la programmazione a oggetti (OOP, Object Oriented Programming), il cui principale vantaggio consisteva nella possibilità di definire delle CLASSI che implementavano una certa funzionalità e/o struttura dati. Il programmatore che utilizzava una certa classe doveva semplicemente richiamare i suoi metodi senza preoccuparsi di come questi implementassero al proprio interno le funzionalità della classe. Questo rendeva un programma modulare, e una classe poteva anche essere interamente riprogrammata (per esempio con un'implementazione più efficiente) senza toccare una sola riga del resto del programma, purché la nuova versione mantenesse gli stessi nomi per metodi e proprietà pubblici della vecchia. Dal punto di vista opposto, una stessa classe poteva essere riusata in più programmi senza modifica alcuna, e questo ha portato alla nascita di una filosofia di programmazione per componenti, e quello dello sviluppo di classi specializzate nella soluzione di certi problemi o nell'implementazione di certe funzionalità è diventato un mercato vero e proprio. Nuovi linguaggi di programmazione sono nati, alcuni come estensione di linguaggi tradizionali (come il C++, derivato dal C e che permette anche una programmazione «mista», o il CLOS, estensione a oggetti del Lisp), altri ex-novo (come Java, anch'esso derivato dal C ma impostato totalmente sulla filosofia OOP).

Ora, a qualche anno di distanza, una nuova filosofia di programmazione sta emergendo, grazie soprattutto alla crescente diffusione di macchine sempre più potenti e di sistemi operativi multitasking, e soprattutto di ambienti distribuiti (reti locali, Internet). Questo scenario, infatti, permette di superare sia la programmazione tradizionale, in cui un programma gira su una macchina, sia la programmazione con approccio client-server, in cui un'applicazione è suddivisa in due o più moduli, che possono risiedere su macchine diverse e dialogano tra loro conservando però una gerarchia ben definita. Infatti, avendo a disposizione un ambiente di rete (sia locale che globale), si possono concepire applicazioni distribuite, formate da una comunità di componenti detti agenti che non solo comunicano fra loro, ma possono anche essere in grado di spostarsi da un computer a un'altro insieme con i propri dati.

La programmazione ad agenti, in effetti, può essere considerata l'applicazione pratica di quei concetti, come la programmazione concorrente e/o distribuita, che per troppo tempo sono rimasti pura teoria, confinata ai corsi universitari come PSN o in alcuni centri di ricerca. Un'applicazione realizzata tramite una popolazione di agenti, infatti, è concorrente in quanto ogni agente può essere composto da più processi o thread (sfruttando le capacità multithread di sistemi operativi come Linux, Windows95/NT, o i prossimi BeOS e Freedows), ma anche distribuita, in quanto l'insieme degli agenti, che girano su macchine diverse di una rete, concorrono a risolvere il problema generale (che può essere di Information Retrieval in ambito Internet/Intranet, di simulazione di processi vari, di monitoraggio, ecc.).

Come risultato del lavoro di molti ricercatori, nell'ambito delle Università e di grandi aziende, sono già apparse diverse librerie utilizzabili per creare applicazioni multiagente. La programmazione ad agenti è basata strettamente su quella a oggetti, in quanto un agente può essere definito in modo molto naturale come un oggetto, o meglio come una classe che incapsula le funzionalità implementate dall'agente, i cui metodi e proprietà pubblici definiscono i servizi che esso mette a disposizione degli altri agenti, servizi realizzati tramite metodi e proprietà privati o protetti. Si può ovviamente usare un qualunque linguaggio a oggetti, ma la natura inerentemente distribuita di un'applicazione multiagente e l'eterogeneità a livello hardware e di sistema operativo che si può incontrare in una rete di computer fanno preferire il linguaggio che per definizione è nato per programmare in ambienti con tali caratteristiche: Java.

Nel seguito si descriveranno due librerie per la programmazione multiagente, entrambe realizzate in Java, che affrontano il problema da due punti di vista, direi complementari: l'Aglets Workbench e la JAFMAS.
 

 

2. Aglets: agenti mobili in un ambiente di rete

Gli IBM Aglets [1] sono oggetti Java che possono vagare sulla rete insieme ai propri dati, spostandosi da un host a un altro insieme ai propri dati, e sono il risultato della ricerca di un gruppo di ricercatori della IBM giapponese. L'architettura di questa libreria è basata su un'API (J-AAPI) che definisce metodi e proprietà di un aglet e dev'essere fornita dalla macchina su cui l'aglet deve girare. Ovviamente, in applicazione del principio del «write once, run everywhere», è ininfluente quale sia l'architettura e/o il sistema operativo della macchina ospite. Il pacchetto comprende un server a interfaccia grafica chiamato Tahiti, che permette di creare, clonare, spostare e compiere altre operazioni sui propri agenti.

2.1. Struttura della J-AAPI

Essa si compone di alcune classi e interfacce e 5 classi che forniscono i metodi e le proprietà per definire un aglet, il suo comportamento, le sue comunicazioni, migrazioni e riproduzioni («cloning»).

Il modello che esse definiscono prevede che un aglet giri come processo autonomo sulla macchina host, che è protetta da aglet che potrebbero danneggiarla dal cosiddetto «contesto», che fornisce un ambiente di esecuzione per l'aglet (questo viene creato da un demone di rete, che inserisce poi in esso gli aglets e può fornire un'interfaccia utente, grafica o a linea di comando); d'altra parte, questo viene schermato da un «proxy» rispetto alla macchina host, sia per impedire l'esecuzione diretta dei metodi degli aglets sia per rendere trasparente all'aglet la sua vera localizzazione. Gli aglets si scambiano messaggi, in maniera sincrona o asincrona, amministrati da un apposito gestore. Un aglet determina il suo itinerario secondo certe tecniche di routing e per tutta la sua esistenza ha un identificatore unico che lo distingue.

2.2. Funzionamento e protocolli d'uso dell'API

Per creare un aglet bisogna prima ottenere il contesto nel quale l'aglet opererà, con il metodo

public final AgletContext getAgletContext()
(notare che questo è final, cioè non può essere ridefinito). In pratica, l'aglet che vuol creare un altro aglet eseguirà la chiamata
getAgletContext().createAglet(getCodeBase(), «SomeAglet», null);
in cui SomeAglet è la classe dell'aglet che è in corso di creazione e al posto di null può esserci una qualche inizializzazione per il nuovo aglet, se occorre. I tre metodi successivi, invocati dal contesto, possono essere personalizzati per dotare l'aglet di un comportamento specifico.
  1. protected Aglet()

  2. È il costruttore dell'aglet, e secondo gli autori non dovrebbe essere ridefinito.

  3. public void onCreation(Object init)

  4. Esegue l'inizializzazione del nuovo aglet, ed è qui che si può agire per personalizzarlo.

  5. public void run()

  6. Dà il via al thread del nuovo aglet, in seguito a creazione, invio, ritiro o attivazione.

In alternativa, si può clonare un aglet esistente per ottenere un suo gemello: In alternativa, si può clonare un aglet esistente per ottenere un suo gemello:
public final Object clone()
Questo metodo serve a ciò: esso clona l'aglet e il suo proxy, restituendo quest'ultimo al chiamante. Tale metodo potrebbe però essere sfruttato per attaccare un sistema ospite determinando l'esaurimento delle sue risorse, dal momento che si potrebbe programmare un aglet per clonare sé stesso all'infinito, e un rimedio, indicato dagli autori, consiste nel far sì che un sistema consenta la clonazione solo di aglets «certificati», di cui il sistema stesso si fida. Questo espediente previene anche un effetto analogo causato da errori di programmazione.
  1. public void onCloning()

  2. Viene chiamato in seguito al tentativo di clonare un aglet. Di default genera un eccezione di tipo SecurityException, e quindi dev'essere ridefinito per eseguire le azioni più appropriate per l'aglet che viene clonato.

  3. public void onClone()

  4. Serve a inizializzare il nuovo clone.

In particolare, 1. viene eseguito dal thread originario, 2. dal thread del clone, con il quale quest'ultimo inizia l'esecuzione. In particolare, 1. viene eseguito dal thread originario, 2. dal thread del clone, con il quale quest'ultimo inizia l'esecuzione.

Al termine dell'esecuzione di un aglet, devono essere deallocate tutte le risorse che il sistema ha riservato per esso e per il suo contesto: questo è importante perché l'esecuzione di solito avviene su server remoti, per i quali la disponibilità di risorse, in termini soprattutto di memoria allocata, è un aspetto critico. La deallocazione avviene tramite il metodo

public final void dispose()
che non può essere ridefinito e uccide tutti i thread dell'aglet che lo chiama e provoca l'invocazione del metodo
public void onDisposing()
che può essere ridefinito per compiere le azioni più appropriate, come chiudere i files e cancellare le finestre che l'aglet ha usato.

Una proprietà fondamentale degli aglets è la loro mobilità, implementata dal metodo

public final AgletProxy dispatch(URL destination)
che invia l'aglet all'indirizzo che prende come argomento. Gli autori usano sempre Agent Transfer Protocol (atp) negli esempi, ma il protocollo effettivamente usato dipende dall'implementazione di J-AAPI e da quelli supportati dal server remoto. La chiamata a dispatch() genera un'altra serie di chiamate. In particolare:
  1. public void onDispatching(URL destination)

  2. permette di personalizzare la fase di preparazione al trasferimento.

  3. public void onArrival()

  4. All'arrivo viene invocato per reinizializzare l'aglet appena trasferito, e anche questo può essere ridefinito.

Un aglet spedito altrove può anche essere richiamato al server di origine con il metodo Un aglet spedito altrove può anche essere richiamato al server di origine con il metodo
retractAglet(someAgletURL)
La chiamata

getAgletContext().retractAglet(someAgletURL);

cerca di richiamare indietro l'aglet specificato dall'URL che prende come argomento. Questo è formato dall'url del server remoto in cui l'aglet sta girando e dall'identificatore dell'aglet stesso:

www.paperopoli.com#identificatore
Come conseguenza viene invocato il metodo
public void onReverting(URL remoteURL)
che può essere personalizzato all'occorrenza (anche per impedire il rientro). RemoteURL è il server da cui proviene la richiesta. Al rientro (se permesso) viene poi invocato OnArrival().

Gli aglets possono anche essere attivati e disattivati, per esempio memorizzandoli in dispositivi secondari.

  1. public final void deactivate(long duration)

  2. Determina la disattivazione di un aglet, che viene rimosso dal contesto e risvegliato dopo un tempo stabilito dall'argomento duration (il sistema può anche violare questa scadenza, in funzione di fattori come il carico).

  3. public void onDeactivating(long duration)

  4. Chiamato in conseguenza di deactivate(), può essere ridefinito per compiere determinate azioni in caso di richiesta di disattivazione.

  5. public void onActivation()

  6. Riattiva un aglet e può essere anch'esso ridefinito in caso di necessità per specificare cosa fare al momento della riattivazione.

2.3. I contesti di esecuzione e le forme di comunicazione

Come detto in precedenza, ogni aglet gira in un contesto, che serve anche a proteggere il sistema da aglet dannosi o malfunzionanti. Un aglet ha accesso al suo contesto tramite il metodo getAgletContext(). È l'interfaccia AgletContext che provvede una serie di metodi per gestire la «vita» di un aglet nel suo contesto e i suoi rapporti con altri aglet ospitati in esso. Il metodo

public abstract AgletProxy createAglet(URL codeBase, String code, Object init)
crea un aglet della classe specificata da code, cercando il file relativo in codeBase, passando poi init a OnCreation(), e restituisce un handle al proxy dell'aglet creato, da usare per manipolazioni successive. Nel caso in cui codeBase sia NULL, la ricerca avviene nelle directory specificate nella variabile di ambiente AGLET_PATH. Da notare che un oggetto di tipo URL permette di specificare sia indirizzi remoti (con prefissi http:, atp:, o quello che sia) sia locali, per mezzo del prefisso file:. In alternativa è possibile usare il metodo getCodeBase() per ottenere la directory in cui si trova il codice del creatore. Il metodo
public abstract Enumeration getAgletProxies()
ottiene una lista degli aglets (di classe Enumeration) appartenenti al contesto corrente, compresi quelli disattivati. Avendo un oggetto Enumeration possiamo usare i metodi hasMoreElements() e nextElement() per testare la fine della lista e leggere il prossimo elemento. I metodi
public abstract AgletProxy getAgletProxy(AgletIdentifier identity)
public abstract AgletProxy getAgletProxy(URL host, AgletIdentifier identity)
restituiscono il proxy di un aglet dato il suo identificatore (nel primo caso) o la coppia URL+identificatore (da usare nel caso di aglet remoto). La chiamata a uno di essi è necessaria per esempio nel caso in cui si voglia comunicare con un aglet, dato che sono i proxy ad occuparsi di questo. Ottenuto il proxy dell'aglet con cui vogliamo comunicare, abbiamo poi alcuni metodi che servono per gestire efettivamente la comunicazione. La coppia
public Object sendMessage(Message message)
public FutureReply sendAsyncMessage(Message message)
è messa a disposizione dal proxy per inviare un messaggio (passato come argomento) in modo rispettivamente sincrono o asincrono. Nel primo caso viene restituita la risposta stessa, nel secondo un handle per ricevere la risposta in seguito. Il metodo
public boolean handleMessage(Message message)
consente di gestire un messaggio ricevuto (il parametro) ed è tipicamente ridefinito dal programmatore. Viene restituito true o false a seconda che il messaggio sia riconosciuto oppure no. Per quanto riguarda i messaggi scambiati, il metodo
public Message(String kind[, ...])
è il costruttore del messaggio, che costituisce un esempio di polimorfismo. I puntini opzionali stanno infatti a indicare un secondo argomento opzionale che, se presente, specifica un ulteriore dato che qualifica il messaggio (di un tipo base), mentre se non è presente crea un oggetto Hashtable. Il primo argomento, obbligatorio, indica invece la «specie» del messaggio. L'oggetto Message ha tre proprietà pubbliche: kind, arg, timeStamp, leggibili con i metodi getKind(), getArg(), getTimeStamp(); la terza proprietà è impostata automaticamente al momento della creazione. Il metodo sameKind(String kind) confronta poi la specie del messaggio che la invoca con la stringa passata come argomento. Se il messaggio creato è di tipo hash table, i metodi
public void setArg(String key, Object value)
public Object getArg(String key)
consentono di manipolarlo; questo tipo di messaggio può essere usato per trasmettere al destinatario più valori (organizzati in coppie ). Per comunicare l'esito dell'elaborazione di un messaggio ricevuto ci sono due metodi:
public void sendReply([...])
invia una risposta, che opzionalmente include un valore (passato come argomento);
public void sendException(Exception exception)
può inviare al contrario un'eccezione, se la gestione del messaggio ha condotto a qualche errore. Se abbiamo inviato un messaggio asincrono (con sendAsyncMessage()), proseguendo poi per i fatti nostri, potremmo a un certo punto dover testare se è stata inviata una risposta. A questo scopo abbiamo a disposizione alcuni metodi. Il primo è
public Object getReply()
che è un metodo dell'oggetto FutureReply restituito da sendAsyncMessage() che sospende il chiamante finché non arriva la risposta al messaggio. I metodi
public boolean isAvailable()
public void waitForReply(long duration)
public void waitForReply()
servono invece a implementare una semantica non bloccante: il primo testa se una risposta è disponibile, gli altri due attendono rispettivamente la durata specificata o l'arrivo del messaggio.

Esiste un apposito gestore che si cura dei messaggi, in particolare esso rende deterministica la loro ricezione da parte del destinatario: questo riceverà due messaggi nello stesso ordine in cui sono stati trasmessi dal mittente (si dice che i messaggi vengono serializzati dal gestore dei messaggi). In questo modo la comunicazione tra due aglets resta deterministica, dato che un messaggio viene inoltrato al destinatario solo quando questo ha terminato l'elaborazione del precedente. Il metodo

public final MessageManager getMessageManager()
permette a un aglet di accedere al proprio gestore dei messaggi, mentre il metodo
public void setPriority(String kind, int priority)
imposta la priorità dei messaggi di specie kind al valore priority, cosicché l'aglet può decidere a quali messaggi dare la precedenza nella coda del gestore. Inoltre il metodo
public void exitMonitor()
fa sì che venga creato un nuovo thread per la gestione dei messaggi che va in parallelo a quello già esistente (un nuovo messaggio può così essere elaborato simultaneamente a quello in corso), mentre i metodi
public void waitMessage()
public void notifyMessage()
public void notifyAllMessages()
servono a gestire la sospensione dell'elaborazione di messaggi: il primo metodo sospende il thread che lo esegue, il secondo e il terzo risvegliano rispettivamente uno e tutti i thread sospesi del corrente gestore di messaggi. Infine il metodo
public void destroy()
termina il gestore dei messaggi. In seguito alla chiamata di questo metodo, tutti i messaggi in coda vengono cancellati e quelli inviati successivamente rifiutati.

È notevole anche la gestione del multicast all'interno di un contesto. I canali multicast hanno comunque la limitazione di essere solo locali, cioè di essere definiti solo nel contesto corrente. Per gestire l'iscrizione di un aglet a gruppi multicast (basati sulla specie dei messaggi) abbiamo tre metodi:

  1. public final void subscribeMessage(String kind)
  2. public final boolean unsubscribeMessage(String kind)
  3. public final void unsubscribeAllMessages()
dei quali 1. iscrive l'aglet che lo chiama al messaggio multicast di specie kind, mentre 2. e 3. cancellano l'iscrizione dell'aglet rispettivamente al messaggio multicast di specie kind e a tutti i messaggi multicast a cui è iscritto. Un messaggio multicast può essere inviato con il metodo dei quali 1. iscrive l'aglet che lo chiama al messaggio multicast di specie kind, mentre 2. e 3. cancellano l'iscrizione dell'aglet rispettivamente al messaggio multicast di specie kind e a tutti i messaggi multicast a cui è iscritto. Un messaggio multicast può essere inviato con il metodo
public void multicastMessage(Message message)
che invia il messaggio specificato come argomento a tutti gli aglets iscritti a quel tipo di messaggio.

2.4. Un possibile utilizzo

Come abbiamo visto, gli aglets sono degli oggetti il cui punto di forza è la mobilità, e che possono essere dotati di un comportamento peculiare tramite il metodo run() nonché tramite i metodi invocati automaticamente in seguito alla creazione, inizializzazione, attivazione/disattivazione, terminazione, invio/arrivo/richiamo (i vari OnXXX()).

Il collante di un insieme di aglets è dato da quello che i loro autori chiamano il «contesto», e questo fatto può essere sfruttato per interessanti applicazioni. Infatti immaginiamo di avere diversi ambienti nei quali vive una popolazione (di agenti mobili, ognuno programmato in un certo modo e quindi dotato di una qualche forma di «intelligenza»). Ogni individuo di questa popolazione abita quindi in un «universo» costituito da più computer collegati in rete.

Lo scenario potrebbe essere questo: un certo particolare ambiente viene implementato in un host da uno o più aglets «fissi», cioè che non si spostano mai (è possibile ottenere questo comportamento ridefinendo il metodo onDispatching() di una classe di aglets in modo tale che come unica azione generi l'eccezione SecurityException). Gli agenti che vivono in tale ambiente sono invece costituiti da aglets mobili, e allora il «mondo» ospitato su un certo host è costituito dal «contesto» che raggruppa gli aglets fissi (l'«ambiente») e quelli mobili eventualmente presenti (gli «agenti»), che possono anche non esserci, cioè quell'ambiente può essere disabitato in un certo istante.

Gli stimoli con cui l'ambiente sollecita gli agenti e le risposte degli agenti che modificano l'ambiente sono allora costituite da un normale scambio di messaggi fra aglets, così come l'interazione fra agenti. Un agente può sperimentare diversi ambienti semplicemente trasferendosi da un host a un altro nella rete (l'«universo» degli agenti), anche pianificando i suoi spostamenti tramite scambio di informazioni con agenti remoti. Il comportamento dell'agente viene specificato, come detto, dai metodi run(), OnXXX() e dal costruttore, e può quindi incorporare qualunque tipo di logica sia rappresentabile con un metodo Java. Per esempio, un agente può decidere di trasferirsi in un host piuttosto che in un altro comunicando con altri agenti che vivono negli host presi in esame ed esaminando le risposte (o in base a esse decidere di non spostarsi affatto). Da notare che l'agente può anche prendere una decisione «sbagliata», cioè, in base alla sua logica e ai dati che riceve dall'esterno, decidere di trasferirsi in un mondo meno «ospitale» di quello in cui ora si trova.

È chiaro allora che agenti dotati di logiche o basi di conoscenza o «necessità» diverse, vivendo e spostandosi fra i vari «mondi» (o «ambienti», cioè hosts) dell'«universo» (cioè la rete) potrebbero trovare collocazioni ideali diverse, o estinguersi, o entrare in competizione con altri agenti per lo stesso ambiente modificando i propri «geni» (cioè la propria logica, conoscenza o necessità). Si potrebbe perciò implementare in una rete di calcolatori un'interessante simulazione dell'evoluzione darwiniana, soprattutto se si riuscisse a realizzare un meccanismo di «mutazione» casuale o ambientale, che dovrebbe entrare in azione per esempio quando un agente si «riproduce» (con i metodi clone() e onClone()).
 
 

 

3. JAFMAS: Java-based Agent Framework for Multiagent Systems

3.1. Generalità
Un limite degli IBM Aglets risiede nella mancanza del supporto a forme di comunicazione «speech-act», nell'assenza di possibilità di coordinazione, cooperazione e coerenza fra i vari agenti di un sistema (che annulla così i vantaggi della programmazione distribuita e del calcolo parallelo che gli agenti permettono) e nell'uso del protocollo TCP-IP che consente solo comunicazioni dirette. La libreria JAFMAS intende risolvere tali difetti, presenti negli aglets così come in molti tool analoghi, ed è quindi incentrata sul coordinamento dei componenti di un programma multiagente: essa è composta di 16 classi Java che mettono a disposizione del programmatore degli strumenti per definire sistemi multiagente in grado di operare mantenendo coerente e consistente lo stato globale della computazione pur senza forme di controllo centralizzato.

In questo quadro viene data enfasi al concetto di «conversazione» fra due o più agenti, cioè il protocollo secondo cui un gruppo di agenti si scambia della conoscenza può essere espresso tramite qualche linguaggio di tipo conversazionale (fra cui KQML [3]). Per giustificare questo approccio si fa ricorso alle reti di Petri, con le quali vengono modellate le conversazioni fra agenti, e di cui viene dato anche un veloce richiamo in [2]. Nelle intenzioni del suo autore, infatti, un'applicazione multiagente programmata con JAFMAS è in pratica una comunità di agenti che «conversano», per esempio con il formalismo KQML, e si coordinano per risolvere il problema dato.

3.2. Utilizzo della libreria

JAFMAS consiste di sedici classi Java e di una metodologia per lo sviluppo di applicazioni. Quest'ultima si compone di 5 passi:

  1. Identificazione degli agenti che comporranno l'applicazione, da raggruppare poi in classi in base alle funzioni svolte e ai servizi messi a disposizione di altri agenti.
  2. Definizione delle conversazioni, cioè dei piani di ogni agente per raggiungere i propri scopi. Poiché il corso delle conversazioni può cambiare in funzione della risposta di altri agenti, esse sono ben rappresentate da automi a stati finiti (ASF).
  3. Definizione delle regole di conversazione: cosa fa l'agente in ogni situazione possibile (=stato dell'ASF).
  4. Analisi del modello delle conversazioni, tramite strumenti come le reti di Petri, per assicurare la coerenza globale della computazione svolta dal sistema multiagente. Eventuali inconsistenze riscontrate possono portare a riformulare le conversazioni stesse (punto 2.).
  5. Implementazione del sistema multiagente in JAFMAS.
Le applicazioni sono suddivise in 3 strati, ognuno dei quali definisce funzionalità ben distinte: Le applicazioni sono suddivise in 3 strati, ognuno dei quali definisce funzionalità ben distinte:
  1. Strato di comunicazione, diviso in due sottostrati.
  2. Modello Sociale, che definisce le regole di conversazione e il Message Router.
  3. Applicazione vera e propria, e a questo livello il programmatore definisce gli agenti. Al di sopra di questo strato c'è poi uno strato di interfaccia con l'operatore.
Lo strato di comunicazione: è composto dalle classi: Lo strato di comunicazione: è composto dalle classi: A un livello superiore si trovano le altre classi. A un livello superiore si trovano le altre classi. Entrambe queste coppie di classi ereditano rispettivamente dalla classe Frame (classi XXXOpInterface) e Canvas (classi XXXCanvas). Entrambe queste coppie di classi ereditano rispettivamente dalla classe Frame (classi XXXOpInterface) e Canvas (classi XXXCanvas).

CreateAgent, Agent, Conversation, ConvRule sono le sole classi che il programmatore deve ridefinire per creare agenti dal comportamento specifico per l'applicazione di interesse, mentre tutti gli aspetti di comunicazione, coordinamento fra agenti e coerenza globale del calcolo sono incapsulati dalle altre classi. In particolare, concretizzando il punto 5. della metodologia prima esposta, si dovranno implementare le suddette quattro classi nel seguente ordine:

CreateAgent:

Agent: Agent:
Il programmatore estende la classe Agent per creare le classi di agenti definite dall'applicazione. Al costruttore dovranno essere passati come parametri (forniti di solito da createAgent()) l'array dei gruppi ai cui l'agente verrà iscritto, il nome dell'agente e l'array delle proprietà. Inoltre, il programmatore dovrà obbligatoriamente ridefinire i metodi astratti:Il programmatore estende la classe Agent per creare le classi di agenti definite dall'applicazione. Al costruttore dovranno essere passati come parametri (forniti di solito da createAgent()) l'array dei gruppi ai cui l'agente verrà iscritto, il nome dell'agente e l'array delle proprietà. Inoltre, il programmatore dovrà obbligatoriamente ridefinire i metodi astratti: Ovviamente, nulla vieta di ridefinire anche altri metodi della classe se necessario. In questa fase saranno definite anche le eventuali classi specifiche per l'applicazione data. Ovviamente, nulla vieta di ridefinire anche altri metodi della classe se necessario. In questa fase saranno definite anche le eventuali classi specifiche per l'applicazione data.

Conversation:
Dovrà essere creata un'estensione di tale classe per ogni automa a stati finiti (rappresentante una conversazione) definito al punto 2. della metodologia di programmazione di JAFMAS. Inoltre si deve ridefinire il metodo

ConvRule: ConvRule:
Implementa concretamente le azioni da compiere in conseguenza dell'esecuzione di una certa regola; questa viene eseguita dal metodo invoke() che controlla prima le condizioni di esecuzione tramite i metodi findRecvdMsgMatch(), suchThat() e waitsForTest(), dopodiché esegue la regola tramite transmit(), doBefore(), doAfter() e waitForConversation(). Tutti i metodi citati possono essere ridefiniti dal programmatore.Implementa concretamente le azioni da compiere in conseguenza dell'esecuzione di una certa regola; questa viene eseguita dal metodo invoke() che controlla prima le condizioni di esecuzione tramite i metodi findRecvdMsgMatch(), suchThat() e waitsForTest(), dopodiché esegue la regola tramite transmit(), doBefore(), doAfter() e waitForConversation(). Tutti i metodi citati possono essere ridefiniti dal programmatore.

4. Altri ambienti di programmazione per sistemi multiagente

Verrà dato per concludere un breve cenno su altri tre pacchetti per la programmazione di applicazioni multiagente, caratterizzati dal fatto di essere 100% Java e di usare estensivamente le caratteristiche della versione 1.1 del JDK (come l'invocazione di metodi remoti, RMI).

4.1 Odyssey

Si tratta del frutto del lavoro della General Magic, arrivato alla beta 2. Questa compagnia sviluppò il linguaggio Telescript per programmare agenti mobili, che ebbe notevole successo, e quando iniziò l'affermazione di Java per la creazione di applicazioni multipiattaforma essa creò l'ambiente Odyssey [4] (in Pure Java), in cui incorporò i concetti già sviluppati per Telescript. Attualmente Odyssey è gratis per usi non commerciali, e GM non ha ancora deciso se diverrà un prodotto commerciale in future release.

Odyssey provvede il supporto per la tecnologia DCOM della Microsoft. Inoltre, l'output di un'applicazione può essere diretto allo standard output, a una finestra oppure a un file sull'host in cui essa viene eseguita, caratteristica questa utile in fase di debugging. Questo però comporta degli inconvenienti: se per esempio l'output è diretto a una finestra, negli intervalli di tempo in cui l'agente non si trova nella macchina virtuale in cui essa è stata definita non si vedrà alcun output di debugging, e questo è dovuto al fatto che Java non ha un supporto nativo per il displaying remoto (meglio sarebbe stato loggare tale output su qualche file durante tali intervalli). Inoltre se l'output di debugging è diretto su un file, l'agente non può muoversi e ogni tentativo di migrare genera un'eccezione, tutto ciò a causa della natura non serializzabile dei file.

Una caratteristica presente ma non ancora documentata è la possibilità per gli agenti di «darsi appuntamento» su particolari macchine della rete. Parimenti non ancora documentata è la possibilità da parte di un agente di «pubblicare» degli oggetti, rendendo disponibili dei riferimenti ad essi a tutti gli altri agenti dell'applicazione. Un limite è inoltre il fatto che solo un server Odyssey può essere eseguito su un dato computer, per cui il testing di applicazioni rende necessario l'accesso a più macchine distinte mentre non è possibile eseguirlo lanciando più istanze della JVM sulla stessa macchina fisica.

4.2 Voyager

È un prodotto ObjectSpace, ora alla versione 1.0 beta 2.1 [5], utilizzabile liberamente per uso interno. Esso consiste di due librerie, voyager.core.jar e voyager.tools.jar (archivi compressi), delle quali solo la prima è necessaria, mentre la seconda contiene dei tools per lo sviluppo (nessun sorgente disponibile).

La comunicazione fra agenti avviene per mezzo del Virtual Object. Voyager dispone del Virtual Code Compiler, che prende una classe (formato sorgente o .class) e la modifica ottenendo una sorta di «mirror» della classe, che può essere istanziato, con il quale si può comunicare e che può essere spostato in una rete. Un oggetto trasformato con VCC assume le caratteristiche di un agente; esso non dispone però necessariamente di un proprio thread, cioè a meno che non sia definito diversamente, esso è solo un oggetto passivo. La comunicazione è sincrona e asincrona, esiste l'invocazione remota di metodi e inoltre riferimenti a oggetti virtuali remoti possono essere passati come parametri. Il server degli agenti, chiamato «voyager», non deve girare necessariamente su tutte le macchine della rete.

Un difetto consiste nel fatto che VCC elabora una classe solo se questa è sintatticamente corretta. Poiché di solito il codice delle classi contiene riferimenti ad altre classi (di oggetti virtuali), questo obbliga il programmatore a definire delle classi vuote per poter compilare correttamente con VCC. Questo dovrebbe essere risolto in futuro, come pure saranno fornite altre caratteristiche come il supporto per il protocollo UDP e la comunicazione multicast e una maggiore attenzione all'aspetto sicurezza.

4.3 Concordia

Si tratta del progetto a più ampio respiro della Horizon [6], una divisione Mitsubishi (non esiste una beta pubblica). Esso utilizza interamente il protocollo RMI per il trasporto e la comunicazione, ed ha un'architettura totalmente modulare. Il modulo fondamentale è il Concordia Server, che fornisce i servizi di comunicazione e l'ambiente di esecuzione degli agenti. In ogni nodo della rete gira uno o più di tali moduli, mentre un solo modulo Administration Manager è necessario in tutta la rete. Quest'ultimo fornisce alcuni servizi come sicurezza, gestione eventi e migrazione di agenti, e per motivi di robustezza o di amministrazione condivisa possono comunque esisterne più di uno nella rete.

Il Persistence Manager si occupa della tolleranza ai guasti, e a tale scopo gestisce le informazioni di stato dei vari agenti nonché il restarting degli stessi. Il Queue Manager invece si occupa della migrazione degli agenti, in tutti i suoi aspetti di mantenimento dello stato (attraverso il Persistence Manager), di nuovi tentativi di migrazione dopo eventuali fallimenti dovuti a disconnessioni o altro, e della priorità con cui gli agenti vengono eseguiti in un particolare server. Questa coppia di moduli quindi fornisce una robusta tolleranza ai guasti, maggiore rispetto ad altri pacchetti per la programmazione ad agenti, e tutto ciò è trasparente (per esempio i tentativi di migrazione o il ripristino dello stato) sia agli agenti che agli amministratori del sistema.

Il modello cooperativo è fornito dal modulo Event Manager, che realizza un modello asincrono in cui gli agenti generano eventi e ascoltano eventi generati da altri agenti. La classe AgentClass poi permette a un gruppo di agenti di cooperare in maniera simile ai meeting di agenti implementati da Odyssey, mentre dovrebbe essere in via di sviluppo il supporto alla comunicazione diretta fra due agenti (bypassando l'Event Manager).

Infine, la sicurezza è realizzata tramite SSL3 (Secure Socket Layer v.3) durante lo spostamento degli agenti e la cifratura dello stato degli agenti durante la loro memorizzazione su supporti persistenti. Ogni agente ha associato anche uno «user id», in modo che il sistema che lo riceve può impostare i giusti permessi di accesso. Tutto ciò è realizzato dal Security Manager, che può supportare anche meccanismi tipo i certificati VeriSign, permettendo così lo sviluppo di applicazioni su rete geografica (anche Internet).

5. Conclusioni

Fra i vari sistemi analizzati il Concordia sembra il più promettente, a causa della sua architettura innovativa, ma rischia di venir penalizzato dal ritardo della sua disponibilità (l'alpha-testing è possibile solo alle organizzazioni commerciali interessate al suo impiego, e non è ancora prevista una licenza per scopi di ricerca e didattici). D'altra parte gli IBM Aglets hanno il punto di forza nella ricchezza della gestione della mobilità degli agenti, mentre JAFMAS (che non è il frutto dello sforzo di un'organizzazione commerciale come gli altri ambienti, ma è stato sviluppato da **** **** nell'ambito delle sue ricerche da tesista a ****) è interessante per gli aspetti della coordinazione degli agenti e del modello conversazionale utilizzato. Solo il tempo potrà quindi dire quali ambienti si affermeranno e in quali settori dello sviluppo software.

6. Bibliografia

Il materiale utilizzato per la stesura di questo articolo proviene in gran parte dalla documentazione presente ai siti [1] e [2]. Purtroppo l'Aglets CookBook presente in [1] è ancora incompleto. In [3] è contenuta la specifica per il formalismo KQML citato al paragrafo 3. su JAFMAS, mentre in [2] viene fornito un breve richiamo delle reti di Petri, nonché l'elenco completo di metodi e proprietà delle classi JAFMAS. In [2] si trova anche una panoramica più articolata delle varie librerie esistenti per la programmazione ad agenti (non tutte implementate in Java), precisamente nel file AgentReview.ps (Attenzione: i documenti contenuti in [2] sono per lo più in formato PostScript, per cui si dovrà disporre di un programma in grado di leggere tale formato, come l'ottimo GhostScript+GhostView sotto Windows).

[1] IBM Aglets - http://www.trl.ibm.co.jp/projects/s7230/aglets/index_e.htm
[2] JAFMAS - http://www.ececs.uc.edu/~abaker/JAFMAS
[3] KQML - http://www.cs.umbc.edu/kqml/
[4] Odyssey - http://www.genmagic.com/agents/ - http://www.genmagic.com/agents/MAF/
[5] Voyager - http://www.objectspace.com/Voyager/
[6] Concordia - http://www.meitca.com/HSL/Projects/Concordia
 
 
 
 
 
 


MokaByte Web  1998 - www.mokabyte.it

MokaByte ricerca nuovi collaboratori. 
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it