Do you want to pick up from where you left of?
Take me there

Параллелизм в OTP

Мы уже рассматривали абстракции языка Elixir для параллельного выполнения, но иногда нужна более широкая функциональность, и тогда мы обращаемся к поведениям OTP.

В этом уроке мы сосредоточимся на GenServer

GenServer

Сервер OTP — это модуль с GenServer, который имплементирует набор функций обратного вызова (callback). На простейшем уровне GenServer — это один процесс, обрабатывающий в цикле по одному запросу за итерацию, сохраняя обновленное состояние.

Для демонстрации API GenServer мы реализуем базовую очередь для хранения и извлечения значений.

Для создания GenServer нужно запустить его и позаботиться об инициализации. В большинстве случаев мы хотим связать процессы, потому используем GenServer.start_link/3. Мы передаем туда запускаемый GenServer модуль, начальные аргументы и набор настроек для самого GenServer. Эти настройки будут переданы в GenServer.init/1, который и задает начальное состояние с помощью возвращаемого значения. В этом примере аргументы и будут начальным состоянием:

defmodule SimpleQueue do
  use GenServer

  @doc """
  Запуск и линковка нашей очереди.
  Это вспомогательный метод.
  """
  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  @doc """
  Функция обратного вызова для GenServer.init/1
  """
  def init(state), do: {:ok, state}
end

Синхронные функции

Часто необходимо взаимодействовать с GenServer в синхронном формате, вызывая функцию и ожидая ее ответа. Для обработки синхронных запросов важно имплементировать метод GenServer.handle_call/3, который передает запрос, PID вызывающего процесса и текущее состояние. Ожидается, что будет возвращен кортеж {:reply, response, state}.

С помощью сопоставления с образцом можно определять функции обратного вызова для разных запросов и состояний. Полный список допустимых вариантов возврата есть в документации GenServer.handle_call/3.

Для демонстрации синхронных запросов давайте добавим возможность отображать текущее состояние очереди и удаление значений:

defmodule SimpleQueue do
  use GenServer

  ### GenServer API

  @doc """
  Функция обратного вызова для GenServer.init/1
  """
  def init(state), do: {:ok, state}

  @doc """
  Функции обратного вызова для GenServer.handle_call/3
  """
  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}

  ### Клиентский API / Функции помощников

  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

Давайте запустим наш SimpleQueue и проверим как работает новая функциональность:

iex> SimpleQueue.start_link([1, 2, 3])
{:ok, #PID<0.90.0>}
iex> SimpleQueue.dequeue
1
iex> SimpleQueue.dequeue
2
iex> SimpleQueue.queue
[3]

Асинхронные функции

Асинхронные запросы обрабатываются функциями обратного вызова handle_cast/2. Это работает приблизительно так же как и handle_call/3, но не получает данных об отправителе и такой метод не обязан ничего отвечать.

Мы реализуем функциональность добавления элемента в очередь асинхронно — обновляя очередь, но не блокируя вызывающий код:

defmodule SimpleQueue do
  use GenServer

  ### GenServer API

  @doc """
  Функция обратного вызова для GenServer.init/1
  """
  def init(state), do: {:ok, state}

  @doc """
  Функции обратного вызова для GenServer.handle_call/3
  """
  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
  """
  def handle_cast({:enqueue, value}, state) do
    {:noreply, state ++ [value]}
  end

  ### Клиентский API / Функции помощников

  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

И попробуем эту новую функциональность:

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]

Для получения дополнительной информации можно обратиться к официальной документации GenServer.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!