Kurz: Elixir-Prozesse sind keine Betriebssystem-Prozesse, sondern winzige Einheiten der BEAM (wenige KB), von denen man Hunderttausende starten kann. Sie teilen keinen Speicher und kommunizieren nur über Nachrichten.
Teil des Kurses Elixir
Prozesse
Elixir-Prozesse sind keine Betriebssystem-Prozesse, sondern winzige Einheiten der BEAM (wenige KB), von denen man Hunderttausende starten kann. Sie teilen keinen Speicher und kommunizieren nur über Nachrichten.
pid = spawn(fn -> IO.puts("Hallo aus einem Prozess") end)
Process.sleep(50)
IO.inspect(is_pid(pid))
IO.inspect(Process.alive?(pid))
eltern = self()
spawn(fn -> send(eltern, {:ergebnis, 6 * 7}) end)
receive do
{:ergebnis, wert} -> IO.puts("Antwort: #{wert}")
after
1000 -> IO.puts("Timeout")
end
# Ein Prozess, der Nachrichten verarbeitet
defmodule Echo do
def schleife do
receive do
{:echo, absender, text} ->
send(absender, {:antwort, String.upcase(text)})
schleife()
:stop ->
:ok
end
end
end
echo = spawn(fn -> Echo.schleife() end)
send(echo, {:echo, self(), "hallo"})
receive do
{:antwort, text} -> IO.puts(text)
end
send(echo, :stop)Ausgabe:
Hallo aus einem Prozess
true
false
Antwort: 42
HALLOViele Prozesse parallel: Task
Task ist die komfortable Abkürzung für “etwas nebenläufig berechnen und das Ergebnis abholen”:
aufgaben = for n <- 1..5, do: Task.async(fn -> Process.sleep(20); n * n end)
IO.inspect(Task.await_many(aufgaben))
ergebnis =
1..20
|> Task.async_stream(fn n -> n * 2 end, max_concurrency: 4)
|> Enum.map(fn {:ok, wert} -> wert end)
|> Enum.sum()
IO.inspect(ergebnis)
# 10.000 Prozesse starten
prozesse = for i <- 1..10_000, do: spawn(fn -> receive do :ende -> i end end)
IO.inspect(length(prozesse))
IO.inspect(Process.info(self(), :message_queue_len))
Enum.each(prozesse, &send(&1, :ende))Ausgabe:
[1, 4, 9, 16, 25]
420
10000
{:message_queue_len, 0}Agent: einfacher Zustand
{:ok, zaehler} = Agent.start_link(fn -> 0 end)
Agent.update(zaehler, &(&1 + 1))
Agent.update(zaehler, &(&1 + 10))
IO.inspect(Agent.get(zaehler, & &1))
Agent.get_and_update(zaehler, fn s -> {s, s * 2} end) |> IO.inspect()
IO.inspect(Agent.get(zaehler, & &1))
tasks = for _ <- 1..100, do: Task.async(fn -> Agent.update(zaehler, &(&1 + 1)) end)
Task.await_many(tasks)
IO.inspect(Agent.get(zaehler, & &1))Ausgabe:
11
11
22
122GenServer: Server-Prozess mit Zustand
Der GenServer (aus OTP, Open Telecom Platform) ist der Standardbaustein für Prozesse mit Zustand, ein Client schickt call (mit Antwort) oder cast (ohne Antwort):
defmodule Warenkorb do
use GenServer
# Client-API
def start_link(opts \\ []), do: GenServer.start_link(__MODULE__, %{}, opts)
def hinzufuegen(pid, artikel, menge \\ 1), do: GenServer.cast(pid, {:hinzufuegen, artikel, menge})
def inhalt(pid), do: GenServer.call(pid, :inhalt)
def anzahl(pid), do: GenServer.call(pid, :anzahl)
def leeren(pid), do: GenServer.call(pid, :leeren)
# Server-Callbacks
@impl true
def init(zustand), do: {:ok, zustand}
@impl true
def handle_cast({:hinzufuegen, artikel, menge}, zustand) do
{:noreply, Map.update(zustand, artikel, menge, &(&1 + menge))}
end
@impl true
def handle_call(:inhalt, _von, zustand), do: {:reply, zustand, zustand}
def handle_call(:anzahl, _von, zustand), do: {:reply, zustand |> Map.values() |> Enum.sum(), zustand}
def handle_call(:leeren, _von, zustand), do: {:reply, zustand, %{}}
end
{:ok, korb} = Warenkorb.start_link()
Warenkorb.hinzufuegen(korb, "Apfel", 3)
Warenkorb.hinzufuegen(korb, "Birne")
Warenkorb.hinzufuegen(korb, "Apfel", 2)
IO.inspect(Warenkorb.inhalt(korb))
IO.inspect(Warenkorb.anzahl(korb))
IO.inspect(Warenkorb.leeren(korb))
IO.inspect(Warenkorb.inhalt(korb))Ausgabe:
%{"Apfel" => 5, "Birne" => 1}
6
%{"Apfel" => 5, "Birne" => 1}
%{}Supervisor: “Let it crash”
Statt Fehler überall abzufangen, lässt man Prozesse abstürzen und Supervisoren sie neu starten. Das macht Systeme robust:
defmodule Fragil do
use GenServer
def start_link(_), do: GenServer.start_link(__MODULE__, 0, name: __MODULE__)
def abstuerzen, do: GenServer.cast(__MODULE__, :boom)
def zaehlen, do: GenServer.call(__MODULE__, :zaehlen)
@impl true
def init(n), do: {:ok, n}
@impl true
def handle_cast(:boom, _), do: raise("Absturz!")
@impl true
def handle_call(:zaehlen, _, n), do: {:reply, n + 1, n + 1}
end
Logger.configure(level: :emergency) # Absturz-Berichte hier ausblenden
{:ok, sup} = Supervisor.start_link([Fragil], strategy: :one_for_one)
IO.inspect(Fragil.zaehlen())
IO.inspect(Fragil.zaehlen())
alt = Process.whereis(Fragil)
Process.flag(:trap_exit, true)
Fragil.abstuerzen()
Process.sleep(100)
neu = Process.whereis(Fragil)
IO.inspect(alt != neu)
IO.inspect(Fragil.zaehlen())
IO.inspect(Supervisor.count_children(sup).active)Ausgabe:
1
2
true
1
1| Strategie | Verhalten |
|---|---|
:one_for_one | nur der abgestürzte Prozess wird neu gestartet |
:one_for_all | alle Kinder werden neu gestartet |
:rest_for_one | der abgestürzte und alle danach gestarteten |
Merke
- Prozesse sind leichtgewichtig, isoliert und kommunizieren per Nachrichten (
send/receive) Taskfür nebenläufige Berechnungen,Agentfür einfachen ZustandGenServerfür Server mit Zustand:call(synchron),cast(asynchron)- Supervisoren starten abgestürzte Prozesse neu: “let it crash”
Übungsaufgabe
Baue einen GenServer, der einen Zähler hält und inkrement und wert anbietet.
Quiz zur Selbstkontrolle
Wie kommunizieren Elixir-Prozesse?
- Über Nachrichten (richtig)
- Über gemeinsame Variablen
- Über globale Objekte
- Über Dateien
Was ist der Unterschied zwischen call und cast?
- call wartet auf eine Antwort, cast nicht (richtig)
- cast wartet auf eine Antwort
- Beide sind asynchron
- Beide sind synchron
Was macht ein Supervisor?
- Startet abgestürzte Prozesse nach einer Strategie neu (richtig)
- Verhindert Abstürze
- Überwacht den Speicher
- Kompiliert Code
Weiter im Kurs
Zurück: Structs, Protokolle, Behaviours und Fehler
Weiter: Mix, Tests und Phoenix
Alle Kapitel: Elixir im Überblick