7 days of WordPress plugins, themes & templates - for free!* Unlimited asset downloads! Start 7-Day Free Trial
Advertisement
  1. Code
  2. Elixir

Was ist GenServer und warum sollte es Sie interessieren?

Read Time: 15 mins

German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)

In diesem Artikel lernen Sie die Grundlagen der Parallelität in Elixir kennen und erfahren, wie Sie Prozesse erzeugen, Nachrichten senden und empfangen sowie lang laufende Prozesse erstellen. Außerdem erfahren Sie mehr über GenServer, sehen, wie es in Ihrer Anwendung verwendet werden kann, und entdecken einige Extras, die es für Sie bereitstellt.

Wie Sie wahrscheinlich wissen, ist Elixir eine funktionale Sprache, mit der fehlertolerante, gleichzeitige Systeme erstellt werden, die viele gleichzeitige Anforderungen verarbeiten. BEAM (Erlang Virtual Machine) verwendet Prozesse, um verschiedene Aufgaben gleichzeitig auszuführen. Dies bedeutet beispielsweise, dass die Bearbeitung einer Anforderung keine andere blockiert. Prozesse sind leichtgewichtig und isoliert, was bedeutet, dass sie keinen Speicher gemeinsam nutzen. Selbst wenn ein Prozess abstürzt, können andere weiterhin ausgeführt werden.

BEAM-Prozesse unterscheiden sich stark von den OS-Prozessen. Grundsätzlich läuft BEAM in einem Betriebssystemprozess und verwendet eigene Scheduler. Jeder Scheduler belegt einen CPU-Kern, wird in einem separaten Thread ausgeführt und kann Tausende von Prozessen gleichzeitig verarbeiten (deren Ausführung abwechselnd ausgeführt wird). Sie können ein bisschen mehr über BEAM und Multithreading auf StackOverflow lesen.

Wie Sie sehen, sind BEAM-Prozesse (ich werde von nun an nur noch "Prozesse" sagen) in Elixir sehr wichtig. Die Sprache bietet Ihnen einige einfache Tools, mit denen Sie Prozesse manuell erzeugen, den Status beibehalten und die Anforderungen bearbeiten können. Sie werden jedoch nur von wenigen Personen verwendet. Es ist üblicher, sich dazu auf das Open Telecom Platform (OTP)-Framework zu verlassen.

OTP hat heutzutage nichts mit Telefonen zu tun - es ist ein universelles Framework zum Erstellen komplexer gleichzeitiger Systeme. Es definiert, wie Ihre Anwendungen strukturiert sein sollen, und bietet eine Datenbank sowie eine Reihe sehr nützlicher Tools zum Erstellen von Serverprozessen, zur Wiederherstellung nach Fehlern, zur Protokollierung usw. In diesem Artikel werden wir über ein Serververhalten namens GenServer sprechen, das von OTP bereitgestellt wird.

Sie können sich GenServer als eine Abstraktion oder einen Helfer vorstellen, der die Arbeit mit Serverprozessen vereinfacht. Zunächst erfahren Sie, wie Sie Prozesse mit einigen Funktionen auf niedriger Ebene erzeugen. Dann werden wir zu GenServer wechseln und sehen, wie es die Dinge für uns vereinfacht, indem wir nicht jedes Mal mühsamen (und ziemlich generischen) Code schreiben müssen. Lassen Sie uns anfangen!

Alles beginnt mit Spawn

Wenn Sie mich fragen würden, wie ein Prozess in Elixir erstellt werden soll, würde ich antworten: Spawn it! spawn/1 ist eine im Kernel-Modul definierte Funktion, die einen neuen Prozess zurückgibt. Diese Funktion akzeptiert ein Lambda, das im erstellten Prozess ausgeführt wird. Sobald die Ausführung abgeschlossen ist, wird auch der Prozess beendet:

Hier hat spawn also eine neue Prozess-ID zurückgegeben. Wenn Sie dem Lambda eine Verzögerung hinzufügen, wird die Zeichenfolge "hi" nach einiger Zeit ausgedruckt:

Jetzt können wir so viele Prozesse erzeugen, wie wir möchten, und sie werden gleichzeitig ausgeführt:

Hier erzeugen wir zehn Prozesse und drucken eine Testzeichenfolge mit einer Zufallszahl aus. :rand ist ein von Erlang bereitgestelltes Modul, daher ist sein Name ein Atom. Was cool ist, ist, dass alle Nachrichten nach fünf Sekunden gleichzeitig ausgedruckt werden. Dies geschieht, weil alle zehn Prozesse gleichzeitig ausgeführt werden.

Vergleichen Sie es mit dem folgenden Beispiel, das dieselbe Aufgabe ausführt, jedoch ohne spawn/1:

Während dieser Code ausgeführt wird, können Sie in die Küche gehen und eine weitere Tasse Kaffee zubereiten, da die Fertigstellung fast eine Minute dauert. Jede Nachricht wird nacheinander angezeigt, was natürlich nicht optimal ist!

Sie könnten fragen: "Wie viel Speicher verbraucht ein Prozess?" Nun, es kommt darauf an, aber anfangs nimmt es ein paar Kilobyte ein, was eine sehr kleine Zahl ist (sogar mein alter Laptop hat 8 GB Speicher, ganz zu schweigen von coolen modernen Servern).

So weit, ist es gut. Bevor wir jedoch mit GenServer arbeiten, wollen wir noch eine weitere wichtige Sache besprechen: das Weiterleiten und Empfangen von Nachrichten.

Arbeiten mit Nachrichten

Es ist keine Überraschung, dass Prozesse (die, wie Sie sich erinnern, isoliert sind) auf irgendeine Weise kommunizieren müssen, insbesondere wenn es darum geht, mehr oder weniger komplexe Systeme aufzubauen. Um dies zu erreichen, können wir Nachrichten verwenden.

Eine Nachricht kann mit einer Funktion mit einem ganz offensichtlichen Namen gesendet werden: send/2. Es akzeptiert ein Ziel (Port, Prozess-ID oder einen Prozessnamen) und die eigentliche Nachricht. Nachdem die Nachricht gesendet wurde, wird sie in der Mailbox eines Prozesses angezeigt und kann verarbeitet werden. Wie Sie sehen, ist die allgemeine Idee unserer täglichen Aktivität, E-Mails auszutauschen, sehr ähnlich.

Eine Mailbox ist im Grunde eine FIFO-Warteschlange (First In First Out). Nachdem die Nachricht verarbeitet wurde, wird sie aus der Warteschlange entfernt. Um Nachrichten zu empfangen, benötigen Sie - raten Sie mal! - ein Empfang-Makro. Dieses Makro enthält eine oder mehrere Klauseln, mit denen eine Nachricht abgeglichen wird. Wenn eine Übereinstimmung gefunden wird, wird die Nachricht verarbeitet. Andernfalls wird die Nachricht wieder in die Mailbox gestellt. Darüber hinaus können Sie eine optionale after-Klausel festlegen, die ausgeführt wird, wenn in der angegebenen Zeit keine Nachricht empfangen wurde. Weitere Informationen zum send/2 und receive finden Sie in den offiziellen Dokumenten.

Okay, genug mit der Theorie - versuchen wir, mit den Nachrichten zu arbeiten. Senden Sie zunächst etwas an den aktuellen Prozess:

Das self/0-Makro gibt eine PID des aufrufenden Prozesses zurück, genau das, was wir brauchen. Lassen Sie nach der Funktion keine runden Klammern weg, da Sie eine Warnung bezüglich der Mehrdeutigkeitsübereinstimmung erhalten.

Erhalten Sie nun die Nachricht, während Sie die after-Klausel festlegen:

Beachten Sie, dass die Klausel das Ergebnis der Auswertung der letzten Zeile zurückgibt, sodass wir das "Hallo!" Zeichenfolge.

Denken Sie daran, dass Sie so viele Klauseln wie nötig einführen können:

Hier haben wir vier Klauseln: eine zur Behandlung einer Erfolgsmeldung, eine zur Behandlung von Fehlern und dann eine "Fallback"-Klausel und eine Zeitüberschreitung.

Wenn die Nachricht keiner der Klauseln entspricht, wird sie in der Mailbox gespeichert, was nicht immer wünschenswert ist. Warum? Denn wenn eine neue Nachricht eintrifft, werden die alten im ersten Kopf verarbeitet (da das Postfach eine FIFO-Warteschlange ist), wodurch das Programm verlangsamt wird. Daher kann eine "Fallback"-Klausel nützlich sein.

Nachdem Sie nun wissen, wie Sie Prozesse erzeugen, Nachrichten senden und empfangen, sehen wir uns ein etwas komplexeres Beispiel an, bei dem ein einfacher Server erstellt wird, der auf verschiedene Nachrichten reagiert.

Arbeiten mit dem Serverprozess

Im vorherigen Beispiel haben wir nur eine Nachricht gesendet, diese empfangen und einige Arbeiten ausgeführt. Das ist in Ordnung, aber nicht sehr funktional. Normalerweise haben wir einen Server, der auf verschiedene Nachrichten antworten kann. Mit "Server" meine ich einen lang laufenden Prozess, der mit einer wiederkehrenden Funktion erstellt wurde. Erstellen wir beispielsweise einen Server, um einige mathematische Gleichungen auszuführen. Es wird eine Nachricht empfangen, die die angeforderte Operation und einige Argumente enthält.

Erstellen Sie zunächst den Server und die Schleifenfunktion:

Wir erzeugen also einen Prozess, der die eingehenden Nachrichten weiter abhört. Nachdem die Nachricht empfangen wurde, wird die Funktion listen/0 erneut aufgerufen, wodurch eine Endlosschleife entsteht. Innerhalb der Funktion listen/0 fügen wir Unterstützung für die Nachricht :sqrt hinzu, mit der die Quadratwurzel einer Zahl berechnet wird. Das arg enthält die tatsächliche Nummer, für die die Operation ausgeführt werden soll. Außerdem definieren wir eine Fallback-Klausel.

Sie können jetzt den Server starten und seine Prozess-ID einer Variablen zuweisen:

Brillant! Fügen wir nun eine Implementierungsfunktion hinzu, um die Berechnung tatsächlich durchzuführen:

Verwenden Sie diese Funktion jetzt:

Im Moment wird einfach das übergebene Argument ausgedruckt. Passen Sie Ihren Code also wie folgt an, um die mathematische Operation auszuführen:

Nun wird eine weitere Nachricht an den Server gesendet, die das Ergebnis der Berechnung enthält.

Interessant ist, dass die Funktion sqrt/2 einfach eine Nachricht an den Server sendet, in der Sie aufgefordert werden, eine Operation auszuführen, ohne auf das Ergebnis zu warten. Im Grunde führt es also einen asynchronen Aufruf durch.

Natürlich möchten wir das Ergebnis irgendwann abrufen, also codieren Sie eine andere öffentliche Funktion:

Verwenden Sie es jetzt:

Es klappt! Natürlich können Sie sogar einen Pool von Servern erstellen und Aufgaben zwischen diesen verteilen, um eine Parallelität zu erreichen. Es ist praktisch, wenn sich die Anforderungen nicht aufeinander beziehen.

Treffen Sie GenServer

In Ordnung, wir haben eine Handvoll Funktionen abgedeckt, mit denen wir lang laufende Serverprozesse erstellen und Nachrichten senden und empfangen können. Das ist großartig, aber wir müssen zu viel Boilerplate-Code schreiben, der eine Serverschleife startet (start/0), auf Nachrichten reagiert (private Funktion listen/0) und ein Ergebnis zurückgibt (grab_result/0). In komplexeren Situationen müssen wir möglicherweise auch einen gemeinsamen Status beibehalten oder die Fehler behandeln.

Wie ich am Anfang des Artikels sagte, besteht keine Notwendigkeit, ein Fahrrad neu zu erfinden. Stattdessen können wir das GenServer-Verhalten verwenden, das bereits den gesamten Code für das Boilerplate für uns bereitstellt und Serverprozesse hervorragend unterstützt (wie wir im vorherigen Abschnitt gesehen haben).

Das Verhalten in Elixir ist ein Code, der ein gemeinsames Muster implementiert. Um GenServer verwenden zu können, müssen Sie ein spezielles Rückrufmodul definieren, das den vom Verhalten vorgegebenen Vertrag erfüllt. Insbesondere sollte es einige Rückruffunktionen implementieren, und die tatsächliche Implementierung liegt bei Ihnen. Nachdem die Rückrufe geschrieben wurden, kann das Verhaltensmodul sie verwenden.

Wie in den Dokumenten angegeben, müssen für GenServer sechs Rückrufe implementiert werden, obwohl auch eine Standardimplementierung vorhanden ist. Dies bedeutet, dass Sie nur diejenigen neu definieren können, für die eine benutzerdefinierte Logik erforderlich ist.

Das Wichtigste zuerst: Wir müssen den Server starten, bevor wir etwas anderes tun können. Fahren Sie also mit dem nächsten Abschnitt fort!

Starten des Servers

Um die Verwendung von GenServer zu demonstrieren, schreiben wir einen CalcServer, mit dem Benutzer verschiedene Operationen auf ein Argument anwenden können. Das Ergebnis der Operation wird in einem Serverstatus gespeichert, und dann kann auch eine andere Operation darauf angewendet werden. Oder ein Benutzer kann ein Endergebnis der Berechnungen erhalten.

Verwenden Sie zunächst das Makro use, um GenServer anzuschließen:

Jetzt müssen wir einige Rückrufe neu definieren.

Das erste ist init/1, das beim Starten eines Servers aufgerufen wird. Das übergebene Argument wird verwendet, um den Status eines anfänglichen Servers festzulegen. Im einfachsten Fall sollte dieser Rückruf das Tupel {:ok, initial_state} zurückgeben, obwohl es andere mögliche Rückgabewerte wie {:stop, reason} gibt, wodurch der Server sofort gestoppt wird.

Ich denke, wir können Benutzern erlauben, den Anfangszustand für unseren Server zu definieren. Wir müssen jedoch überprüfen, ob das übergebene Argument eine Zahl ist. Verwenden Sie dazu eine Schutzklausel:

Starten Sie nun einfach den Server mit der Funktion start/3 und stellen Sie Ihren CalcServer als Rückrufmodul bereit (das erste Argument). Das zweite Argument wird der Ausgangszustand sein:

Wenn Sie versuchen, eine Nicht-Nummer als zweites Argument zu übergeben, wird der Server nicht gestartet. Genau das benötigen wir.

Großartig! Nachdem unser Server ausgeführt wird, können wir mit der Codierung mathematischer Operationen beginnen.

Asynchrone Anforderungen bearbeiten

Asynchrone Anforderungen werden im Sinne von GenServer als Casts bezeichnet. Verwenden Sie zum Ausführen einer solchen Anforderung die Funktion cast/2, die einen Server und die eigentliche Anforderung akzeptiert. Es ähnelt der Funktion sqrt/2, die wir codiert haben, wenn wir über Serverprozesse sprechen. Es wird auch der "Feuer und Vergessen"-Ansatz verwendet, was bedeutet, dass wir nicht darauf warten, dass die Anforderung beendet wird.

Um die asynchronen Nachrichten zu verarbeiten, wird ein Rückruf handle_cast/2 verwendet. Es akzeptiert eine Anforderung und einen Status und sollte im einfachsten Fall mit einem Tupel {:noreply, new_state} antworten (oder {:stop, reason, new_state}, um die Serverschleife zu stoppen). Nehmen wir zum Beispiel eine asynchrone :sqrt-Besetzung:

So erhalten wir den Status unseres Servers. Anfänglich war die Nummer (übergeben, als der Server gestartet wurde) 5.1. Jetzt aktualisieren wir den Status und setzen ihn auf :math.sqrt(5.1).

Codieren Sie die Schnittstellenfunktion, die cast/2 verwendet:

Für mich ähnelt dies einem bösen Zauberer, der einen Zauber wirkt, sich aber nicht um die Auswirkungen kümmert, die er verursacht.

Beachten Sie, dass wir eine Prozess-ID benötigen, um die Umwandlung durchzuführen. Denken Sie daran, dass beim erfolgreichen Start eines Servers ein Tupel {:ok, pid} zurückgegeben wird. Verwenden wir daher den Mustervergleich, um die Prozess-ID zu extrahieren:

Nett! Der gleiche Ansatz kann verwendet werden, um beispielsweise eine Multiplikation zu implementieren. Der Code wird etwas komplexer, da wir das zweite Argument, einen Multiplikator, übergeben müssen:

Die cast-Funktion unterstützt nur zwei Argumente, daher muss ich ein Tupel erstellen und dort ein zusätzliches Argument übergeben.

Nun der Rückruf:

Wir können auch einen einzelnen handle_cast-Rückruf schreiben, der den Betrieb unterstützt und den Server stoppt, wenn der Vorgang unbekannt ist:

Verwenden Sie nun die neue Schnittstellenfunktion:

Großartig, aber derzeit gibt es keine Möglichkeit, ein Ergebnis der Berechnungen zu erhalten. Daher ist es Zeit, einen weiteren Rückruf zu definieren.

Synchrone Anforderungen bearbeiten

Wenn es sich bei asynchronen Anforderungen um Casts handelt, werden synchrone Anforderungen als Aufrufe bezeichnet. Verwenden Sie zum Ausführen solcher Anforderungen die Funktion call/3, die einen Server, eine Anforderung und ein optionales Zeitlimit akzeptiert, das standardmäßig fünf Sekunden entspricht.

Synchrone Anforderungen werden verwendet, wenn wir warten möchten, bis die Antwort tatsächlich vom Server eintrifft. Der typische Anwendungsfall besteht darin, einige Informationen wie das Ergebnis von Berechnungen abzurufen, wie im heutigen Beispiel (denken Sie an die Funktion grab_result/0 aus einem der vorherigen Abschnitte).

Um synchrone Anforderungen zu verarbeiten, wird ein handle_call/3-Rückruf verwendet. Es akzeptiert eine Anforderung, ein Tupel, das die PID des Servers enthält, und einen Begriff, der den Anruf identifiziert, sowie den aktuellen Status. Im einfachsten Fall sollte es mit einem Tupel {:reply, reply, new_state} antworten.

Code diesen Rückruf jetzt:

Wie Sie sehen, nichts Komplexes. Die reply und der neue Status entsprechen dem aktuellen Status, da ich nach der Rückgabe des Ergebnisses nichts mehr ändern möchte.

Nun das Schnittstelle result/1 Funktion:

Das ist es! Die endgültige Verwendung des CalcServers wird nachfolgend demonstriert:

Aliasing

Es wird etwas mühsam, beim Aufrufen der Schnittstellenfunktionen immer eine Prozess-ID anzugeben. Glücklicherweise ist es möglich, Ihrem Prozess einen Namen oder einen Alias zu geben. Dies erfolgt beim Start des Servers durch Festlegen name:

Beachten Sie, dass ich pid jetzt nicht speichere, obwohl Sie möglicherweise einen Mustervergleich durchführen möchten, um sicherzustellen, dass der Server tatsächlich gestartet wurde.

Jetzt werden die Schnittstellenfunktionen etwas einfacher:

Vergessen Sie jedoch nicht, dass Sie nicht zwei Server mit demselben Alias starten können.

Alternativ können Sie eine weitere Schnittstellenfunktion start/1 in Ihr Modul einführen und das Makro __MODULE __ /0 nutzen, das den Namen des aktuellen Moduls als Atom zurückgibt:

Beendigung

Ein weiterer Rückruf, der in Ihrem Modul neu definiert werden kann, heißt terminate/2. Es akzeptiert einen Grund und den aktuellen Status und wird aufgerufen, wenn ein Server beendet werden soll. Dies kann beispielsweise passieren, wenn Sie ein falsches Argument an die Schnittstellenfunktion multiply/1 übergeben:

Der Rückruf könnte ungefähr so aussehen:

Schlussfolgerung

In diesem Artikel haben wir die Grundlagen der Parallelität in Elixir behandelt und Funktionen und Makros wie spawn, receive und send erläutert. Sie haben gelernt, was Prozesse sind, wie man sie erstellt und wie man Nachrichten sendet und empfängt. Außerdem haben wir gesehen, wie ein einfacher Serverprozess mit langer Laufzeit erstellt wird, der sowohl auf synchrone als auch auf asynchrone Nachrichten reagiert.

Darüber hinaus haben wir das Verhalten von GenServer erörtert und festgestellt, wie es den Code durch die Einführung verschiedener Rückrufe vereinfacht. Wir haben mit den Rückrufen init, terminate, handle_call und handle_cast gearbeitet und einen einfachen Berechnungsserver erstellt. Wenn Ihnen etwas unklar erschien, zögern Sie nicht, Ihre Fragen zu posten!

GenServer bietet noch mehr, und natürlich ist es unmöglich, alles in einem Artikel zu behandeln. In meinem nächsten Beitrag werde ich erklären, was Supervisoren sind und wie Sie sie verwenden können, um Ihre Prozesse zu überwachen und sie nach Fehlern wiederherzustellen. Bis dahin viel Spaß beim Codieren!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Scroll to top
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.