Tracks
/
Elixir
Elixir
/
Lehrplan
/
GenServer
Ge

GenServer in Elixir

4 Übungen

Über GenServer

GenServer (generischer Server) ist ein Verhalten, das die üblichen Client-Server-Interaktionen zwischen Elixir-Prozessen abstrahiert.

Erinnerst du dich noch an die receive-Schleife aus der Lektion über Prozesse? Das GenServer-Verhalten bietet Abstraktionen, um solche Schleifen zu implementieren und um Nachrichten mit einem Prozess auszutauschen, der eine solche Schleife ausführt. Damit ist es einfacher, den Zustand zu halten und asynchronen Code auszuführen.

Note

Sei gewarnt: Der Name GenServer ist mehrdeutig belegt. Er beschreibt auch ein Modul, das das GenServer-Verhalten nutzt, sowie einen Prozess, der aus einem Modul gestartet wurde, das das GenServer-Verhalten nutzt.

Das GenServer-Verhalten definiert einen erforderlichen Callback, init/1, und einige interessante optionale Callbacks: handle_call/3, handle_cast/2 und handle_info/3. Die Clients, die einen GenServer verwenden, sollen diese Callbacks nicht direkt aufrufen. Stattdessen bietet das GenServer-Modul Funktionen, mit denen Clients mit einem GenServer-Prozess kommunizieren können.

Häufig definiert ein einzelnes Modul sowohl eine Client-API, also eine Reihe von Funktionen, die andere Teile deiner Elixir-App aufrufen können, um mit diesem GenServer-Prozess zu kommunizieren, als auch Server-Callback-Implementierungen, die die Logik dieses GenServer enthalten.

Schauen wir uns zuerst ein einfaches Beispiel für einen GenServer an und lernen dann, was die einzelnen Callbacks bedeuten.

Beispiel

Das ist ein Beispielserver, der auf die wiederholten Fragen nerviger Mitfahrer auf einer langen Autofahrt reagieren kann, genauer gesagt auf die Frage: „Sind wir schon da?“. Er zählt mit, wie oft diese Frage gestellt wurde, und gibt zunehmend genervtere Antworten.

defmodule AnnoyingPassengerAutoresponder do
  use GenServer
  # Client API

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end

  def are_we_there_yet?(pid) do
    GenServer.call(pid, :are_we_there_yet?)
  end

  # Server callbacks

  @impl GenServer
  def init(_init_arg) do
    # the initial count of questions asked is always 0
    state = 0
    {:ok, state}
  end

  @impl GenServer
  def handle_call(:are_we_there_yet?, _from, state) do
    reply =
      cond do
        state <= 3 -> "No."
        state <= 10 -> "I told you #{state} times already. No."
        true -> "..."
      end

    # increase the count of questions asked
    new_state = state + 1
    # reply to the caller
    {:reply, reply, new_state}
  end
end

Callbacks

init/1

Ein Server kann gestartet werden, indem du GenServer.start/3 oder GenServer.start_link/3 aufrufst. Den Unterschied zwischen diesen Funktionen haben wir im Links-Konzept kennengelernt.

Diese beiden Funktionen:

  • Erwarten als erstes Argument ein Modul, das das GenServer-Verhalten implementiert.
  • Akzeptieren als zweites Argument einen beliebigen Wert namens init_arg. Wie der Name vermuten lässt, wird dieses Argument an den init/1-Callback übergeben.
  • Akzeptieren ein optionales drittes Argument mit erweiterten Optionen zum Ausführen des Prozesses, auf die wir jetzt nicht eingehen.

Wenn du einen Server mit GenServer.start/3 oder GenServer.start_link/3 startest, wird der init/1-Callback blockierend aufgerufen. Der Rückgabewert von init/1 entscheidet, ob der Server erfolgreich gestartet werden kann.

Der init/1-Callback gibt üblicherweise einen dieser Werte zurück:

  • {:ok, state}. Der Server startet seine receive-Schleife und verwendet state als Anfangszustand. state kann einen beliebigen Typ haben.
  • {:stop, reason}. reason kann einen beliebigen Typ haben. Der Server startet seine receive-Schleife nicht. Der Prozess beendet sich mit dem angegebenen Grund.

Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.

Wenn die receive-Schleife des Servers startet, geben die Funktionen GenServer.start/3 und GenServer.start_link/3 ein Tupel {:ok, pid} zurück. Andernfalls geben sie {:error, reason} zurück.

handle_call/3

Eine Nachricht, die eine Antwort erfordert, kann mit GenServer.call/2 an einen Serverprozess gesendet werden. Diese Funktion erwartet als erstes Argument die pid eines laufenden Serverprozesses und als zweites Argument die Nachricht. Die Nachricht kann einen beliebigen Typ haben.

Der handle_call/3-Callback ist dafür zuständig, synchrone Nachrichten zu verarbeiten und zu beantworten. Er erhält drei Argumente:

  1. message – der Wert, der als zweites Argument an GenServer.call/2 übergeben wurde.
  2. from – die pid des Prozesses, der GenServer.call/2 aufruft. Meistens kannst du dieses Argument ignorieren.
  3. state – der aktuelle Zustand des Servers. Denk daran, dass sein Anfangswert im init/1-Callback gesetzt wurde.

Der handle_call/3-Callback gibt üblicherweise ein 3-Tupel {:reply, reply, state} zurück. Das bedeutet, dass das zweite Element des Tupels, ein reply beliebigen Typs, an den Aufrufer zurückgeschickt wird. Das dritte Element des Tupels, state, ist der neue Zustand des Servers nach der Verarbeitung dieser Nachricht.

Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.

Note

Um dir anhand des Namens zu merken, was dieser Callback tut, stell dir vor, du rufst jemanden am Telefon an.

Wenn diese Person erreichbar ist, bekommst du sofort eine Antwort (synchron).

handle_cast/2

Eine Nachricht, die keine Antwort erfordert, kann mit GenServer.cast/2 an einen Serverprozess gesendet werden. Ihre Argumente sind dieselben wie bei GenServer.call/2.

Der handle_cast/2-Callback ist dafür zuständig, diese Nachrichten zu verarbeiten. Er erhält zwei Argumente, message und state, dieselben wie beim handle_call/3-Callback (außer from).

Der handle_cast/2-Callback gibt üblicherweise ein 2-Tupel {:noreply, state} zurück.

Es gibt noch weiter fortgeschrittene Möglichkeiten, die wir jetzt nicht behandeln.

Note

Um dir anhand des Namens zu merken, was dieser Callback tut, denk daran, dass „to cast“ auch „werfen“ bedeutet.

Wenn du eine Flaschenpost ins Meer wirfst, erwartest du nicht sofort eine Antwort, vielleicht nie.

Sollte ich call oder cast verwenden?

Verwende fast immer call, auch wenn dein Client-Code die Antwort des Servers nicht braucht.

Bei call wartest du auf die Antwort, was als Backpressure-Mechanismus dient (damit Clients nicht zu viele Nachrichten auf einmal senden). Eine Antwort vom Server zu erhalten ist außerdem die einzige Möglichkeit, sicher zu sein, dass der Server die Nachricht des Clients empfangen und verarbeitet hat.

handle_info/2

Nachrichten können auch auf anderem Weg als über GenServer.call/2 oder GenServer.cast/2 im Posteingang des Servers landen, zum Beispiel durch einen direkten Aufruf der Funktion send/2.

Um solche Nachrichten zu verarbeiten, verwendest du den handle_info/2-Callback. Dieser Callback funktioniert genauso wie handle_cast/2.

Das GenServer-Verhalten bietet eine Catch-all-Implementierung von handle_info/2, die Fehler über unerwartete Nachrichten protokolliert. Wenn du diese Standardimplementierung überschreibst, achte darauf, immer eine eigene Catch-all-Implementierung einzufügen. Wenn du das vergisst, stürzt der Server ab, sobald er eine unerwartete Nachricht erhält.

Timeouts

Der Rückgabewert jedes der vier oben beschriebenen Callbacks kann um ein weiteres Tupel-Element erweitert werden, ein Timeout. Statt zum Beispiel aus init/1 ein {:ok, state} zurückzugeben, gibst du {:ok, state, timeout} zurück.

Das Timeout kannst du nutzen, um festzustellen, dass über einen bestimmten Zeitraum keine Nachrichten im Postfach eingehen. Wenn der Server aus einem seiner Callbacks ein Timeout zurückgibt und die angegebene Anzahl Millisekunden verstrichen ist, ohne dass eine Nachricht eintrifft, wird handle_info/2 mit :timeout als erstem Argument aufgerufen.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne GenServer

Das Üben ist gesperrt

Schalte 2 weitere Übungen frei, um GenServer zu üben