Programmieren mit Erlang – Teil 3: Besonderheiten

Mittlerweile ist schon wieder einige Zeit vergangen seit dem letzten Teil unserer kleinen Erlang-Serie. Vergessen haben wir den dritten versprochenen Teil aber natürlich nicht.
Thema dieses Teils soll die Realisierung der nebenläufigen und parallelen sowie auch verteilten Programmierung sein, genauso wie ein kurzer Ausflug in die Problembehebung beim Absturz eines Servers oder Prozesses. Somit soll es also um eine Thematik gehen, die dem Programmierer in den üblichen verdächtigen Programmiersprachen ungleich mehr Aufwand bereitet, als Programme mit lediglich einem Prozess. In Erlang ist dies dagegen kein allzu großes Problem. Mithilfe spezieller Sprach-Konstrukte lässt sich dies mit nur minimalem Aufwand realisieren.

Wie kann ich weitere Prozesse anlegen?
Überraschend einfache lässt sich in Erlang ein weiterer Prozess anlegen. Notwendig ist letztendlich nur der Aufruf einer kleinen internen Funktion: spawn. Die einfachste Variante wäre hierbei folgende:
[erlang]spawn(Fun)[/erlang] Hier wird also als einziger Parameter eine Höhere Funktion (siehe den 2. Teil dieser Reihe) ohne Parameter benötigt. Zurückgegeben wird eine eindeutige ID des damit gestarteten Prozesses (die ID des aktuellen Prozesses lässt sich im Übrigen mittels self() herausfinden).
Eine etwas komplexere Möglichkeit wäre folgender Aufruf:
[erlang]spawn(Modul, Funktion, Parameter)[/erlang] Hier können also gleich drei Parameter angegeben werden: Zuerst der Name des Moduls der entsprechenden Funktion, dann der Name der Funktion und zuletzt die Parameter, die für den Aufruf der Funktion benötigt werden – in Form einer Liste. Im Folgenden werden wir aber bei ersterer Variante bleiben.
Somit ist also schon einmal bekannt, wie man einen neuen Prozess startet. Ein Prozess endet dagegen in der Regel, sobald die zu berechnende Funktion terminiert.
Die komplette Nutzung von Mehr-Kern-Prozessoren macht hierbei keinen zusätzlichen Aufwand – darum kümmert sich Erlang intern (auch wenn die Erlang-Prozesse nicht mit den Betriebssystem-Prozessen verwechselt werden sollten).

Und können die Prozesse untereinander kommunizieren?
Ganz einfach: Ja, das können sie. Ein Prozess kann einem anderen Prozess eine Nachricht senden über das folgende Konstrukt: [erlang]Pid ! Nachricht[/erlang] Hierbei entspricht Pid der ID des entprechenden Prozesses, an den die Nachricht gesendet werden soll und Nachricht einem ganz normalen Pattern, wie wir es bereits aus den Funktionsdefinitionen und ähnlichen Konstrukten kennen.
Natürlich wird auch noch ein Konstrukt für das Empfangen von Nachrichten benötigt. Dies ist ein wenig komplexer als das Versenden – schließlich kann ein Prozess mehrere verschiedene Nachrichten gesendet bekommen und sollte die bestenfalls auch unterscheiden können. Daraus ergibt sich folgendes:
[erlang]receive
Pattern1 -> Anweisungsblock1;
Pattern2 when Guard -> Anweisungsblock2;

PatternN -> AnweisungsblockN
after Time -> Anweisungsblock
end[/erlang]
Letztlich unterscheidet sich der receive-Block also kaum von dem case-of-Block. Lediglich eine Stelle ist wirklich neu: Vor dem end kann die Anweisung after eingefügt werden (muss Übrigens aber nicht zwingend), deren Anweisungsblock nach einer angegebenen Zeit Time (in Millisekunden) ausgeführt wird, wenn keine der Patterns auf eine eingetroffene Nachricht matchen – wird keine after-Anweisung eingefügt, so wartet der Prozess hier solange, bis eine Nachricht eintrifft, die auf eines der Patterns matcht.
Sollten dagegen schon mehrere Nachrichten, angekommen sein, bevor der Prozess beim receive-Block angelangt ist, so werden die Nachrichten in der Reihenfolge, in der sie eintrafen, auf das Matchen überprüft.

Als Beispiel, wie das Ganze nun live funktioniert, wollen wir einen Server entwerfen, der alles auf der Konsole ausgibt, was ihm von wem gesendet wird. Um angeben zu können, wer (d.h. welche Prozess-ID) dem Server eine Nachricht geschickt hat, schicken wir hierbei ganz einfach die Prozess-ID mit der Nachricht zusammen in einem 2-Tupel:
[erlang]-module(printServer).
-export([init/0,send/2,stop/1]).

init() -> spawn(fun loop/0).
send(Pid, Message) -> Pid ! {self(), Message}.
stop(Pid) -> Pid ! stop.

loop() -> receive
{Pid, Message} ->
io:format(“~p schrieb: ~p~n”, [Pid, Message]),
loop();
stop -> true
end.[/erlang]
Dies sieht dann in der Konsole wie folgt aus (mit Ausgabe von Erlang):
[erlang]> Pid = printServer:init().
<0.39.0>

> printServer:send(Pid, “hier steht eine Nachricht”).
<0.32.0> schrieb: “hier steht eine Nachricht”

> printServer:stop(Pid).
stop[/erlang]

Und geht das vielleicht auch ohne diese komischen Prozess-IDs?
Ja, auch dies ist möglich. Mit Hilfe der Funktion register(irgendEinAtom, PID) kann einem Atom eine Prozess-ID eindeutig zugewiesen werden. Dieses Atom ist in allen Prozessen des Erlang-Knotens verfügbar und kann entweder mithilfe von unregister(irgendEinAtom) wieder freigegeben werden. Alternativ wird beim Beenden eines Prozesses auch das zugeordnete Atom wieder freigegeben.
Somit kann also auch wie folgt eine Nachricht versendet werden:
[erlang]irgendEinAtom ! Nachricht[/erlang]
Des Weiteren kann mittels whereis(irgendEinAtom) die Prozess-ID herausgefunden werden, die dem Atom zugewiesen wurde.

Und wie läuft das nun auf verteilten Systemen?
Die kurze Antwort: Nicht wesentlich anders. Da jedoch nicht jedermann zwei Rechner zu Hause stehen hat (ich auch nicht), erklären wir es im Folgenden mit zwei simulierten Erlang-Knoten an einem Rechner. Zunächst benötigen wir zwei Erlang-Betriebssystem-Prozesse. Dazu starten zwei Betriebssystem-Konsolen (im Folgenden Konsole 1 und Konsole 2) und starten Erlang mit den Parametern -sname und -setcookie: [erlang]erl -sname hierDerName -setcookie hierDerCookie[/erlang] Man beachte, dass der Wert von -sname in beiden Fenstern verschieden sein sollte und von -setcookie gleich. Somit haben wir soeben zwei Erlang-Clienten (hierDerName1@rechnerName und hierDerName2@rechnername) geschaffen, die bisher aber noch nichts voneinander wissen (Test mit Hilfe der Funktion nodes(): Gibt auf beiden Konsolen [] aus). Nun kann mittels folgendem Aufruf eine Funktion auf einem anderen Erlang-Knoten ausgeführt werden: [erlang]rpc:call(Knoten, Modul, Funktion, Parameter).[/erlang] Der erste Parameter entspricht hierbei dem Namen des Erlang-Knotens, auf dem die Funktion ausgeführt werden soll, die restlichen Parameter haben wir weiter oben in diesem Artikel bereits erläutert. In Konsole 1 geben wir also beispielsweise folgendes ein: [erlang]rpc:call(‘hierDerName2@rechnerName’, io, format, [“Hier der Text”]).[/erlang]
Auf dem ersten Blick scheint die Funktion nun doch anders als erwartet, nicht auf Konsole 2, sondern dennoch auf Konsole 1 ausgegeben zu haben. Jedoch wurde die Funktion io:format() auf Konsole 2 berechnet. Beim erneuten Aufruf von nodes() werden nun im Übrigen der jeweils andere Knoten aufgelistet – also sind unsere beiden Knoten nun endlich miteinander verbunden.
Die oben genannte Eingabe macht nun nicht wirklich viel Sinn. Viel sinnvoller wäre beispielsweise, die Prozess-ID mittels whereis (Modul ist hier im Übrigen ganz einfach erlang) aus einem Atom vom anderen Knoten herauszufinden, mit dem dann wie mit einem Prozess im selben Knoten kommuniziert werden kann.
Hier das Beispiel von oben für ein verteiltes System:
[erlang]-module(printServer).
-export([init/0,send/2,stop/1]).

init() -> PID = spawn(fun loop/0),
register(printer, PID).
send(Pid, Message) -> Pid ! {self(), Message}.
stop(Pid) -> Pid ! stop.

loop() -> receive
{Pid, Message} ->
io:format(“~p schrieb: ~p~n”, [Pid, Message]),
loop();
stop -> true
end.[/erlang]
Auf Konsole 1 gebe man nun folgendes ein:
[erlang]> printerServer:init().
true

>printerServer:send(printer, “irgendeine Nachricht”).
<0.32.0> schrieb: “irgendeine Nachricht”[/erlang]
Nun kann man auf Konsole 2 folgendes eingeben:
[erlang]> PID = rpc:call(‘hierDerName1@rechnerName’, erlang, whereis, [printer]).
<5824.53.0>

> printerServer:send(PID, “andere Nachricht”).[/erlang]
Und schon erscheint auf Konsole 1 folgende Ausgabe: [erlang]<5722.38.0> schrieb: “andere Nachricht”[/erlang]
Natürlich weiß die Konsole 2 an dieser Stelle nichts darüber, ob ihre Nachricht letztendlich überhaupt auf Konsole 1 ausgegeben wurde. Dies könnte dann beispielsweise durch eine Bestätigungs-Nachricht vom Ausgabe-Server zur Konsole 2 erledigt werden. Jedoch würde das den Rahmen dieses Eintrags sprengen.

Und wenn einmal ein Prozess abstürzt?
In Verteilten System ist es kein Ding der Unmöglichkeit, dass einmal ein Knoten abstürzt (vor allem, wenn da ein bestimmtes Betriebssystem aus dem amerikanischen Redmond mit im Spiel ist :-D). Bevor es dann zu weiteren Problemen kommt, sollen deshalb möglicherweise alle Benutzer an den anderen Knoten über den Ausfall dieses Knotens benachrichtigt werden oder der ausgefallene Prozess soll an anderer Stelle einfach neugestartet werden. All dies lässt sich in Erlang erneut mit nur wenig Aufwand programmieren. Dazu ist es dem Programmierer möglich, einen Prozess mit einem (oder mehreren) anderen zu verbinden und auf eine Sterbe-Nachricht dieser Prozesse zu warten – Erlang hat nämlich die Eigenschaft beim Tot eines seiner Prozesse oder des gesamten Knotens eine Nachricht mit der Todes-Ursache an alle verlinkten (lauschenden) Prozesse zu versenden.
Das Verlinken von Prozessen kann ganz einfach mittels link(Pid) realisiert werden und mittels process_flag(trap_exit, true) lässt sich angeben, dass der momentane Prozess über den Tod des verlinkten Prozess benachrichtigt werden soll. Die Sterbe-Nachricht wird dann letztendlich über eine Prozess-Nachricht in der Form {‘EXIT’, Pid, Grund} zum Empfänger gesendet, die er dann via receive abrufen kann.
Dies kann letztendlich so in etwa aussehen: [erlang]exitHandler(Pid) ->
process_flag(trap_exit, true),
link(Pid),
receive
{‘EXIT’, Pid, Grund} -> io:format(“Der Prozess ~p starb mit der Nachricht: ~p~n”, [Pid, Grund]
end.[/erlang]
Diese Funktion wird nun einfach einem neuen Prozess zugeordnet. Und schon wird beim Tod des verlinkten Prozesses die in Zeile 5 angegebene Nachricht auf der Konsole ausgegeben.

Damit sind wir auch schon am Ende dieses Artikels und der Erlang-Reihe im Ganzen angekommen. Ich hoffe, die Programmiersprache Erlang damit dem einen oder anderen doch etwas näher gebracht zu haben, sodass er/sie sich vielleicht noch etwas vertieft mit Erlang beschäftigt. Die genannten Methoden hier und auch in den vorigen Teilen waren letztendlich nur Beispiele. Es gibt auch noch genügend weitere Möglichkeiten, die genannten Probleme zu realisieren, die jedoch den Umfang dieses Artikels gesprengt hätten und letztendlich auch nicht mehr sonderlich spannend gewesen wären. Schlussendlich ist Erlang eine kleine, nette Sprache, an dessen ungewöhnliche Syntax und Semantik man sich natürlich erst gewöhnen muss, aber meiner Meinung nach lohnt sich die Mühe durchaus. Diverse Software-Hersteller haben dies auch bereits erkannt und verwenden Erlang in dem Gebiet, wo es am Stärksten ist – in parallelen und verteilten Systemen. Jedoch ist auch anzumerken, dass Erlang gerade für einfache Probleme – z.B. Sortieren oder Suchen – viel Aufwand benötigt und in der Regel auch nicht sonderlich effektiv ist. Der Programmierer sollte also wie immer schon im Voraus wissen, mit welchem Problem er konfrontiert ist, und damit entscheiden, ob Erlang für das Projekt geeignet ist…

Quelle: Teile dieses Artikels wurden inspiriert von Prof. Schäfers und Prof. Sattlers Vorlesung “Programmierparadigmen” an der TU Ilmenau sowie der Dokumentation auf erlang.org.

Ein Kommentar
  1. Vielleicht nochmal eine kleine Anmerkung bezüglich den Prozess-IDs:
    Die ID ist innerhalb eines Knotens immer eindeutig. IDs von einem Prozess auf einem Knoten sehen auf einem anderen Knoten wieder anders aus.

    Allgemein lässt sich sagen, dass IDs desselben Knotens die Form <0.x.0> besitzen (x ist irgendeine beliebige Zahl), IDs von einem externen Knoten dagegen (y und z sind beliebige Zahlen, wobei y nicht 0 ist).
    In der Regel muss sich der Programmierer aber nicht darum kümmern, dass die Prozess-IDs in verteilten Programmen konvertiert werden müssen. Darum kümmert sich bereits Erlang selber.

    Antworten
    31. Juli 2012, 12:40

Schreibe einen Kommentar zu Chris Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*