Manche Dinge will man gar nicht wissen
Gestern haben wir damit begonnen, ein Zahlenrate-Spiel zu entwickeln. Allerdings mussten wir dabei wissen, wie wir die beiden Aktoren zusammenschalten müssen, damit das Spiel funktioniert. Das ist sehr unflexibel. Heute ist die Gelegenheit diese Unschönheit auszubügeln. Wünschenswert wäre, wenn wir nur einen Aktor erzeugen müssten, der das gesamte Spiel steuert.
Das klingt eigentlich ganz einfach. Wir fügen einen Game Aktor hinzu, der die Aufgaben erfüllt, die wir vorher in der Kommandozeilen Anwendung hatten. Außerdem lassen wir die Aktoren nicht sofort loslaufen, sondern senden ihnen Nachrichten, mit denen deren Aktivität implizit gestartet wird.
Die nachfolgenden Zeilen sehen zunächst richtig aus, sind aber falsch. Schauen wir uns den Code einmal an von dem ich spreche und diskutieren wir das verborgene Problem. Also Niemals, wirklich niemals so etwas hier schreiben bitte:
// innerhalb eines Aktors -- FALSCH! var actor1 = Context.ActorOf(...); var actor2 = Context.ActorOf(...); // spricht mit actor1 actor1.Tell(new Start()); actor2.Tell(new Start()); // kommt evtl. vorher
Wo ist das Problem eigentlich? Der Code wird zwar sequentiell ausgeführt, aber die Nachrichten, die wir den Aktoren actor1 und actor2 in deren Mailbox gestellt haben, erheben keinen Anspruch darauf, Aktor übergreifend in richtiger Reihenfolge ausgeführt zu werden. Lediglich innerhalb eines Aktors ist die Reihenfolge garantiert. Schlimmer wird das noch, wenn Aktoren verteilt sind. Dann treten teilweise messbare Latenzen auf, die solch ein Verhalten noch schlimmer machen können.
Angewendet auf unser Zahlenratespiel könnte das bedeuten, dass der Chooser Aktor (der die zufällige Zahl auswählt) noch gar keine Wahl getroffen hat, aber bereits eine erste Anfrage erhält. Wenn er erst einmal seine Zahl gewählt hat, werden die Antworten dann vielleicht anders ausfallen, als das vorher der Fall war. Das Spiel wird möglicherweise nicht terminieren.
Um solche Überraschungen zu vermeiden, erweitern wir unser Protokoll. Eine typische Unterhaltung zwischen unseren Aktoren wird dann in dieser Reihenfolge stattfinden:
// zu Beginn Game -> Chooser: Start (Chooser wählt eine Zufallszahl) Chooser -> Game: Started Game -> Enquirer: Start // wiederholt, solange Zahl nichterraten ist Enquirer -> Chooser: TestTry(x) Chooser -> Enquirer: TooBig(x) | TooSmall(x) | Guessed(x) // wenn erraten Chooser -> Game: Guessed(x)
Leider fallen die Änderungen, die wir heute vornehmen müssen, etwas zu umfangreich für einen blog aus. Daher ist der aktuelle Stand in einem github Repository zu finden.
Im Gegensatz zur gestrigen Version hat die heutige Veränderung den Vorteil, dass wir nicht mehr wissen müssen, wie wir das gesamte Spiel in Gang bekommen. Es startet sich einfach so:
var system = ActorSystem.Create("Numb3rs"); var game = system.ActorOf(Props.Create<GuessGame>(), "Game"); game.Tell(new Start());
Was folgern wir daraus?
Jede zusätzliche Funktionalität könnte in einem neuen Aktor erfolgen. Allerdings verkompliziert sich das Protokoll mit jedem weiteren Aktor. Das können wir wiederum kompensieren, indem wir erneut einen Aktor "vorschalten" der die zusätzliche Komplexität kapselt.
Ist unsere Anwendung damit komplett?
Naja, das kommt darauf an... Führt man diese kleine Anwendung auf einem einzelnen Computer aus, kann man sicher mit einer hundert prozentigen Quote an erfolgreich zugestellten Nachrichten rechnen. Aber stell Dir einmal etwas komplexere Protokolle vor, die über mehrere Systeme verteilt laufen. Was würde passieren, wenn z.B. der Chooser in eine Exception läuft?
Auch wenn das Beispiel zu klein dafür ist, werden wir uns die Details dafür morgen zu Gemüte führen.
Keine Kommentare:
Kommentar veröffentlichen