Współbieżność z OTP
Poznaliśmy już abstrakcję do obsługi współbieżności, jaką oferuje Elixir, ale czasami potrzebujemy większej kontroli nad tym, co się dzieje, dlatego też możemy chcieć użyć zachowań OTP, na których zbudowany jest Elixir.
W tej lekcji skupimy się na istotniejszym elemencie: GenServer
GenServer
Serwer OTP zawiera moduł zachowań GenServer, który implementuje zestaw wywołań zwrotnych (ang. callback). W dużym uproszczeniu GenServer to pętla, w której każda iteracja odpowiada obsłudze jednego żądania, które aktualizuje stan aplikacji.
Zademonstrujemy działanie API GenServer, implementując prostą kolejkę.
By uruchomić GenServer, musimy go wystartować oraz obsłużyć procedurę inicjacji.
W większości przypadków chcemy obsłużyć łączenie procesów, dlatego używamy GenServer.start_link/3
.
Przekazujemy do modułu GenServer argumenty startowe i zestaw opcji.
Argumenty zostaną przekazane do funkcji GenServer.init/1
, która na ich podstawie utworzy stan początkowy poprzez zwracaną przez nią wartość.
W naszym przykładzie argumenty i stan początkowy będą takie same:
defmodule SimpleQueue do
use GenServer
@doc """
Uruchom naszą kolejkę i połącz jej proces.
Jest to funkcja pomocnicza.
"""
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
@doc """
GenServer.init/1 callback
"""
def init(state), do: {:ok, state}
end
Funkcje synchroniczne
Często zadania zlecane GenServerom muszą być wykonywane w sposób synchroniczny — po wywołaniu funkcji czekamy na rezultat.
By obsłużyć synchroniczne żądania, musimy zaimplementować funkcję zwrotną GenServer.handle_call/3
, która jako parametry przyjmuje: żądanie, PID procesu wywołującego, stan; oczekiwana odpowiedź to z kolei krotka w postaci: {:reply, odpowiedź, stan}
.
Wykorzystując dopasowania wzorców, możemy zdefiniować wiele różnych wywołań zwrotnych, w zależności od żądania i stanu.
Pełna dokumentacja zawierająca listę parametrów i zwracanych wartości znajduje się w dokumentacji GenServer.handle_call/3
.
By zademonstrować wywołanie synchroniczne, dodajmy do naszej kolejki możliwość wyświetlenia zawartości i usunięcia jednej z wartości:
defmodule SimpleQueue do
use GenServer
### GenServer API
@doc """
GenServer.init/1 callback
"""
def init(state), do: {:ok, state}
@doc """
GenServer.handle_call/3 callback
"""
def handle_call(:dequeue, _from, [value | state]) do
{:reply, value, state}
end
def handle_call(:dequeue, _from, []), do: {:reply, nil, []}
def handle_call(:queue, _from, state), do: {:reply, state, state}
### Client API / Helper functions
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def queue, do: GenServer.call(__MODULE__, :queue)
def dequeue, do: GenServer.call(__MODULE__, :dequeue)
end
Wystartujmy naszą aplikację SimpleQueue
i przetestujmy jej nowe funkcje:
iex> SimpleQueue.start_link([1, 2, 3])
{:ok, #PID<0.90.0>}
iex> SimpleQueue.dequeue
1
iex> SimpleQueue.dequeue
2
iex> SimpleQueue.queue
[3]
Funkcje asynchroniczne
Wywołania asynchroniczne są obsługiwane przez handle_cast/2
.
Działają podobnie jak handle_call/3
, a jedynymi różnicami są brak PID wywołującego oraz to, że nie oczekujemy żadnej odpowiedzi.
Zaimplementujemy dodawanie elementów do kolejki jako funckję asynchroniczną, dzięki czemu dodając element nie będziemy blokować działania programu:
defmodule SimpleQueue do
use GenServer
### GenServer API
@doc """
GenServer.init/1 callback
"""
def init(state), do: {:ok, state}
@doc """
GenServer.handle_call/3 callback
"""
def handle_call(:dequeue, _from, [value | state]) do
{:reply, value, state}
end
def handle_call(:dequeue, _from, []), do: {:reply, nil, []}
def handle_call(:queue, _from, state), do: {:reply, state, state}
@doc """
GenServer.handle_cast/2 callback
"""
def handle_cast({:enqueue, value}, state) do
{:noreply, state ++ [value]}
end
### Client API / Helper functions
def start_link(state \\ []) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def queue, do: GenServer.call(__MODULE__, :queue)
def enqueue(value), do: GenServer.cast(__MODULE__, {:enqueue, value})
def dequeue, do: GenServer.call(__MODULE__, :dequeue)
end
Spróbujmy użyć naszej nowej funkcji:
iex> SimpleQueue.start_link([1, 2, 3])
{:ok, #PID<0.100.0>}
iex> SimpleQueue.queue
[1, 2, 3]
iex> SimpleQueue.enqueue(20)
:ok
iex> SimpleQueue.queue
[1, 2, 3, 20]
Więcej informacji znajdziesz w oficjalnej dokumentacji GenServer.
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!