Über Autonomie, Asynchronität und andere AMETAS-Geschichten

Michael Zapf

Agenten und Dienste in AMETAS zu programmieren ist eine sehr interessante Aufgabe. Wichtig dabei ist jedoch das Einhalten gewisser Entwurfsprinzipien:

Es gibt - wie in vielen Fällen - auch hier das Problem, sich eher vom Konzept oder eher von einer effizienten Programmierung leiten zu lassen. Beides ist selten möglich: Effiziente Programmierung ist häufig dadurch geprägt, daß konzeptionelle Strukturen trickreich umgangen oder ausgenutzt werden. Der vordergründige Zugewinn wird später durch Inkompatibilität mit konzeptkonformen Erweiterungen relativiert.
 

Aufrufe.

AMETAS-Agenten sind so gestaltet, daß sie keinen Zugriff auf Objekte außer ihrem eigenen Treiber erhalten. So kann verhindert werden, daß sie Methoden unberechtigterweise auf dem Stellenobjekt oder anderen Objekten aufrufen.

Um mit anderen Agenten oder mit Diensten Daten auszutauschen,  werden Nachrichten verwendet. Diese werden durch das Postfachsystem jeder einzelnen Stelle verwaltet.

Wenn ein Agent auf eine Funktionalität zugreifen soll, die ihm an einer Stelle geboten wird, bieten sich daher zwei Mechanismen an:

Natürlich bietet sich die zweite Möglichkeit nur den Entwicklern der AMETAS-Plattform an und ist mit einer Versionsänderung verbunden. Es wäre so etwa denkbar, daß gewisse Funktionen sehr effizient aufgerufen werden können. Zusätzliche Erweiterungen von anderer Seite ist im Prinzip nur durch AMETAS-Dienste möglich. Dies erfordert u.a. eine ausführliche Nachrichtenverarbeitung usw. Damit ist klar, daß die Effizienz leidet, was nicht zu vermeiden ist.
 

Asynchronität/Autonomie.

AMETAS bietet die Möglichkeit, Agenten autonom zu programmieren. Da keine Schnittstellen auf Sprachebene existieren, ist eine Bindung an einen Aufrufer nicht möglich. Konzeptionell sauber wäre es, in regelmäßigen Abständen nach neu eingetroffenen Nachrichten zu sehen. Sind welche vorhanden, kann der Agent entscheiden, ob und wann er sie verarbeiten will. Wenn nicht, wird der Auftrag in einer bestimmten Weise fortgesetzt.
Diese fortgesetzte Abfrage (auch Polling) beeinträchtigt in erheblicher Weise die Effizienz der Agentenanwendung. Wenn die Rate zu hoch gewählt wird, verlangsamt sich die gesamte Abwicklung dieses Agenten wie auch aller anderen; ist sie zu niedrig, dann leidet die Reaktivität.

Ein konzeptioneller Ausweg ist der Einsatz von Sensoren. Diese sind in AMETAS durch das Ereignisverarbeitungssystem (Event Handling) zum Teil repräsentiert: Wenn Nachrichten eintreffen, "spürt" der Agent deren Eintreffen durch die Ausführung einer speziellen EventHandler-Methode. Der Haupt-Programmablauf kann dann entsprechend reagieren.

Wie muß das Agentenprogramm überhaupt gestaltet werden, wenn man Events verarbeiten möchte?

  1. Der Agent muß sein Interesse an einem Eventtyp der Stelle mitteilen.

  2. Der Agent muß eine oder mehrere Methoden implementieren, die im Falle eines Ereignisses ausgeführt werden sollen.

  3. Der Programmfluß des Agenten muß durch den eingetroffenen Event beeinflußt werden.

Um einen Programmfluß zu ändern, gibt es in Java - wie in vielen anderen Sprachen auch - den if...then...else-Konstrukt (und ähnliche andere Konstrukte wie while). Aufgrund der Nichtvorhersagbarkeit des Ereigniszeitpunkts - bedingt durch die Asynchronität - ist es erforderlich, die if-Anweisung wiederholt auszuführen, bis ein Ereignis eintritt (oder eben while zu benutzen).  Diese wiederholte Abfrage einer Zustandsvariablen ist jedoch wiederum eine Variante des Polling, was wir hier als inneres Polling bezeichnen wollen. Allerdings läuft das innere Polling wesentlich schneller und effizienter ab.

Um dieses innere Polling zu verhindern, wird häufig folgende Strategie angewendet: Das PlaceUserDriver-Objekt, auf das der Agent Zugriff hat, erlaubt das Anhalten des Treiberthreads durch die Methode suspendThr, die ihrerseits Thread.suspend aufruft.  In dieser Weise findet man dies in der Klasse AMETASPlaceUserDriver in der Methode requestService_WaitAndUnregister.
 

... 
  depositMessage(...);  
  suspendThr();  
     // Nachricht eingetroffen; jetzt auswerten. 
...

public boolean handleMessageEvent(...) { 
  // Nachricht auswerten 
  resumeThr(); 
}

 
Diese Technik funktioniert , obwohl der Agent nur einen treibenden Thread besitzt (im Treiber): Der Thread, der für die Ausführung von handleMessageEvent verantwortlich zeichnet, läuft im EventManager der Stelle. Er sorgt dafür, daß die Programmausführung des Agenten fortgesetzt wird.
 
Natürlich entbehrt diese Strategie nicht einer gewissen Eleganz: Der Agent wartet ohne allzu großem Ressourcenverbrauch, bis eine bestimmte Nachricht eingetroffen ist. Es gibt weder inneres noch äußeres Polling. Im Prinzip handelt es sich um eine Synchronisation der asynchronen Nachrichten. Jedoch ist sie für die Entwicklung von Agenten  nicht zu empfehlen: Der Agent verliert die Kontrolle über sich, ohne eine Chance zu besitzen, sie wiederzuerlangen. Es ist - zieht man einen Vergleich zur realen Welt - als würde man sich eine Narkose verpassen lassen. Man geht fest davon aus, irgendwann wieder aufzuwachen. Oder? Wer hat sich schon einmal in Narkose versetzen lassen, um das Warten auf ein bestimmtes Ereignis zu verkürzen?

Eine geeignete Implementierung könnte so aussehen:
 

... 
  depositMessage(...);   // Agent schickt Nachricht ab 
... 
  while ((!m_bAnswerArrived) && (!bGivenUp)) { 
     // unternimm irgendwas in der Zwischenzeit, bis die Antwort eintrifft. 
  } 
...

  if (bGivenUp) { 
     //  Antwort kam nicht an; Plan muß geändert werden 
  } 

  else { 
     // alles klar, Antwort verarbeiten und mit dem Plan fortfahren 
  } 
...

public boolean handleMessageEvent(...) { 
     // Nachricht auswerten 
  m_bAnswerArrived = true; 
}

 
Konzept hin, Effizienz her - es geht hier nicht darum, irgendwelche hehre Prinzipien hochzuhalten, die eh ein Großteil der Agentenprogrammierer nicht konsequent umsetzen. Man könnte fragen: Was nützen die besten Konzepte, wenn sie unpraktikabel sind? Oder: Wer wird es auf sich nehmen, so umständlich zu programmieren, wenn es auch einfach geht? Und wenn AMETAS es erzwingt, aber das BestAgentSystemEver es eben erlaubt... Daher lautet mein wesentlicher Einwand gegen die "einfache" Lösung: Der Agent hat keine Möglichkeit, zu entscheiden, ob die erwartete Nachricht jemals eintreffen wird.

Es sollte Teil der Konzeption eines Agenten sein, angesichts asynchroner Nachrichten, deren Verarbeitung insbesondere durch einen anderen Agenten nicht gesichert ist, Maßnahmen zu definieren, die im Falle des Nichteintreffens einer Antwort erfolgen müssen.

Eine ähnliche Vorgehensweise findet man in der Sprache Java durch das Ausnahmen-Konzept (Exceptions) im Falle von Fehlerzuständen. Eine mögliche Alternative zur obigen Lösung kann das "Schlafen" sein (Thread.sleep); damit ist immerhin die Chance gegeben, daß der Programmablauf innerhalb des Agenten fortschreitet.
 

Das Konzept

Das AMETAS-Agentensystem ist ein System aus Objekten unterschiedlicher Kategorien; dabei sind stationäre Objekte (Stellen und Dienste, auch Benutzeradapter) und bewegliche Objekte (Agenten) zu unterscheiden. Die Verhaltensbescheibung jedes Objekts ist generell unsichtbar; der Zustand ist privat (bis auf Teile, die allen Objekten eigen sind). Alle diese Objekte können in ihrem Inneren beliebige Verfahren einsetzen, um Informationen zu verarbeiten; zwischen den Objekten werden Informationen über Nachrichten ausgetauscht. Die Zustellung von Nachrichten obliegt der Infrastruktur des Systems. Der Transit einer Nachricht ist für die Objekte transparent; sie verfügen über keine Informationen (insbesondere die Dauer) bezüglich des Transits.

Im Kontext objektorientierter Sprachen spricht man in den Modellierungen im übrigen ebenfall von Nachrichten, die zwischen den Objekten ausgetauscht werden. Realisiert werden diese Nachrichten über Prozeduraufrufe. Im Unterschied dazu manifestiert sich das Informationsfluß-Modell in AMETAS in Form von tatsächlich vorhandenen Strukturen. Rechnet man Prozeduraufrufe (genauer: Methodenaufrufe) mit zu den Nachrichten, dann kommt man nicht umhin, zwei Sorten von Nachrichten zu unterscheiden.

Streitbare Punkte. Im Rahmen der Fortentwicklung der Infrastruktur fällt auf, daß ein Teil der von Agenten nachgefragten Funktionen durch Methodenaufrufe auf dem Treiber, ein anderer Teil durch AMETAS-Nachrichten genutzt werden. Beispiel: Ein Agent löst seine Migration durch Aufruf von AMETASAgentDriver.go(...) aus. Eine Alternative wäre es, die Stelle selbst zum Kommunikationspartner zu machen, die Nachrichten verstehen kann - zumal sie Nachrichten an Agenten senden kann.

Ein anderes Beispiel ist der PlaceNameService: Derzeit wird er "nebenher" von AMETAS-Stellen erledigt, ohne in das Agentensystem integriert zu sein. Zwar ist so die Implementierung einfacher; aber angenommen, man schreibt einen Konfigurationsagenten, der Daten vom PlaceNameService benötigt. Möchte man gänzlich auf die Erstellung eines Dienstes verzichten, müßte man den PlaceUserDriver erweitern. Allerdings benötigen externe Schnittstellen wie AMSI und AMAI einen PlaceNameService, ohne in das Agentensystem eingebunden zu sein.

Es ist klar, daß nicht alle Methodenaufrufe durch Nachrichten ersetzt werden können. Es muß letztendlich eine Methode zum Nachrichtensenden und -empfangen geben. Ein Kompromiß wäre es, einen "Wrapper"-Dienst zu entwerfen, der einige dieser Stellenfunktionen (auch die go-Methode!) als Dienst den Agenten zur Verfügung stellt. Den Entwicklern bleibt freigestellt, diesen Dienst zu verwenden oder auf die bestehenden Methoden zurückzugreifen.
 

Und was nun?

Zugegebenermaßen erscheint es häufig unumgänglich, einen Agenten auf eine Antwort warten zu lassen, bis diese endlich eintrifft. Genauso kann die ständige Verwendung asynchroner Nachrichten sehr aufwendig erscheinen, und man würde sich in einigen Fällen wünschen, doch irgendwie einen Methodenaufruf zur Verfügung zu haben. Jedoch handelt es sich bei Agenten nicht nur um ein neues Konstrukt in der objektorientierten Welt, sondern um ein neues Programmierparadigma. Das heißt, daß zu erwarten ist, daß die Umstellung konventioneller Anwendungen durchaus ein hohes Maß an Aufwand - wenn nicht sogar einen völligen Neuentwurf - erfordern kann. Ähnliches galt bereits für den Wechsel zur objektorientierten Programmierung: Ein C++-Compiler übersetzt eben auch C-Programme.

Fazit: Es obliegt jedem Entwickler, seine Agenten mehr oder weniger zur AMETAS-Sichtweise konform zu programmieren und die gegebenen Freiräume zu nutzen. Es kann noch lange nicht davon gesprochen werden, daß das Paradigma in allen Einzelheiten festgelegt ist. Warnen muß man jedoch davor, bestehende Konstrukte nicht-agentenbasierter Anwendungen in das Agentensystem zu pressen. Das Resultat kann nur heißen: Es läuft viel langsamer als vorher, dafür ist es viel aufwendiger.

Nach meinem Ermessen spielen Asynchronität und Autonomie die zentrale Rolle beim Entwurf von Agentenanwendungen - in ähnlicher Weise, wie Ableitung, Methoden usw. für C++ oder andere OO-Sprachen zentral sind.


mz, 03.11.98