В чём разница между GenServer, Agent и Task?
В Elixir модули GenServer, Agent и Task предоставляют разные абстракции для работы с процессами, каждая из которых предназначена для решения конкретного класса задач. Все они основаны на акторной модели Erlang и взаимодействуют с системой через процессы BEAM. Ниже — подробное сравнение, особенности и назначение каждого из них.
GenServer — универсальный сервер с пользовательской логикой
Описание:
GenServer (Generic Server) — это наиболее гибкая абстракция, позволяющая реализовать любое состояние и поведение процесса. Подходит для сложных серверов, взаимодействия с другими процессами, обработки сообщений, реализации кэшей, очередей и сервисов.
Особенности:
-
Поддерживает handle_call/3, handle_cast/2, handle_info/2;
-
Может работать синхронно и асинхронно;
-
Полностью контролирует жизненный цикл процесса;
-
Управление состоянием (state);
-
Встроенная поддержка таймеров (:timeout);
-
Используется в деревьях супервизоров.
Пример:
defmodule Counter do
use GenServer
def start_link(initial_value), do: GenServer.start_link(\__MODULE_\_, initial_value, name: \__MODULE_\_)
def increment(), do: GenServer.cast(\__MODULE_\_, :inc)
def get(), do: GenServer.call(\__MODULE_\_, :get)
def init(val), do: {:ok, val}
def handle_cast(:inc, state), do: {:noreply, state + 1}
def handle_call(:get, \_from, state), do: {:reply, state, state}
end
Когда использовать:
-
Нужно сложное управление состоянием;
-
Есть необходимость обрабатывать сообщения, ошибки, таймеры;
-
Требуется настраиваемое поведение при сбоях и остановке.
Agent — упрощённый интерфейс для хранения состояния
Описание:
Agent — это лёгкий инструмент для хранения и изменения состояния. Он создаёт процесс, хранящий значение, и предоставляет API для его безопасного доступа и изменения.
Особенности:
-
Не обрабатывает произвольные сообщения;
-
Нет кастомных handle_* колбэков;
-
Нет логики внутри агента — только операции над состоянием;
-
Подходит для простых кэшей, счётчиков, флагов.
Пример:
{:ok, pid} = Agent.start_link(fn -> 0 end, name: :my_counter)
Agent.update(:my_counter, fn state -> state + 1 end)
Agent.get(:my_counter, fn state -> state end)
Когда использовать:
-
Нужно просто хранить значение и изменять его;
-
Нет сложной логики обработки сообщений;
-
Нужна высокая производительность и простота.
Task — выполнение одноразовой асинхронной операции
Описание:
Task используется для асинхронного выполнения кода в отдельных процессах. Подходит для запуска фоновых задач, параллельной загрузки данных, обработки ввода-вывода и работы с API.
Особенности:
-
Task.start — запускает задачу, не заботясь о результате;
-
Task.async/await — запускает задачу и позволяет дождаться результата;
-
Поддерживает таймауты и отмену;
-
Может быть включён в супервизор.
Пример:
task = Task.async(fn -> :math.pow(2, 10) end)
result = Task.await(task)
Когда использовать:
-
Нужно выполнить функцию в фоне и получить результат;
-
Требуется параллельная загрузка или вычисление;
-
Задача выполняется один раз и быстро завершается.
Сравнительная таблица
Характеристика | GenServer | Agent | Task |
---|---|---|---|
Основное назначение | Универсальное поведение | Простое состояние | Одноразовая задача |
--- | --- | --- | --- |
Хранит состояние | Да | Да | Нет (если только не внутри) |
--- | --- | --- | --- |
Сложная логика обработки | Да | Нет | Нет |
--- | --- | --- | --- |
Обработка сообщений | Да | Нет | Нет |
--- | --- | --- | --- |
Асинхронные операции | Да | Да | Да |
--- | --- | --- | --- |
Используется в OTP | Да | Да | Да |
--- | --- | --- | --- |
Поддержка таймеров | Да | Нет | Ограниченно |
--- | --- | --- | --- |
Участие в супервизии | Да | Да | Да |
--- | --- | --- | --- |
Управление через API | GenServer.call/cast | Agent.get/update | Task.async/await |
--- | --- | --- | --- |
Подходит для кэша | Да | Да | Нет |
--- | --- | --- | --- |
Подходит для I/O задач | Да | Нет | Да |
--- | --- | --- | --- |
Подходит для фоновых задач | Да | Нет | Да |
--- | --- | --- | --- |
Итого по применению:
-
Используй Agent, когда нужен простой безопасный доступ к состоянию без логики.
-
Используй GenServer, когда нужно настраиваемое поведение, многопоточность, состояние, взаимодействие, таймеры и т.д.
-
Используй Task, когда нужно выполнить функцию параллельно и получить результат, особенно при работе с I/O.