В чём разница между 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.