Что такое send и receive?

В Elixir конструкции send и receive используются для организации межпроцессного взаимодействия. Поскольку в Elixir нет общего состояния и процессы изолированы друг от друга, единственный способ обмена данными между ними — через передачу сообщений.

send/2

send отправляет сообщение другому процессу. Сигнатура:

send(pid, message)
  • pid — идентификатор процесса, которому адресовано сообщение.

  • message — любое значение: атом, строка, число, кортеж, список, структура и т.д.

Пример:

pid = self()
send(pid, :hello)

Здесь текущий процесс отправляет себе сообщение :hello.

receive

receive используется для получения сообщений, которые были отправлены процессу через send. Он реализует паттерн-матчинг по входящим сообщениям:

receive do
:hello -> IO.puts("Привет получен")
:bye -> IO.puts("Пока")
end

Если в "почтовом ящике" процесса есть сообщение, которое соответствует одному из шаблонов, оно будет извлечено и обработано.

Очередь сообщений

Каждый процесс в Elixir имеет свою очередь сообщений (mailbox). Все сообщения, отправленные в процессе его жизни, накапливаются в этой очереди до тех пор, пока не будут обработаны через receive. Порядок обработки соответствует порядку отправки (FIFO), но receive может пропустить сообщения, если шаблон не совпадает.

Использование кортежей для сообщений

На практике часто используют кортежи, чтобы удобно обрабатывать сообщения и дополнительно включать отправителя:

send(self(), {:msg, "текст", 123})
receive do
{:msg, text, id} -> IO.puts("Получено: #{text}, id: #{id}")
end

Пример отправки между процессами

defmodule Messenger do
def listen do
receive do
{:ping, sender} ->
send(sender, :pong)
end
listen()
end
end
pid = spawn(Messenger, :listen, \[\])
send(pid, {:ping, self()})
receive do
:pong -> IO.puts("Получен pong")
end

Здесь создаётся процесс Messenger, который ждёт сообщение {:ping, sender} и отвечает :pong.

Таймаут в receive

Можно задать таймаут в миллисекундах:

receive do
:hello -> IO.puts("Привет")
after
2000 -> IO.puts("Таймаут!")
end

Если за 2 секунды не пришло подходящего сообщения — выполнится блок after.

Игнорирование сообщений

Если receive не содержит шаблона для определённого сообщения, оно останется в почтовом ящике. Это может вызвать "засорение" очереди, особенно если такие сообщения не обрабатываются позже.

Расширенные шаблоны с условиями

Можно использовать when для добавления условий:

receive do
{:data, x} when is_integer(x) and x > 10 ->
IO.puts("Получено число больше 10: #{x}")
end

Пример с несколькими шаблонами

receive do
{:log, msg} -> IO.puts("Лог: #{msg}")
{:error, reason} -> IO.puts("Ошибка: #{reason}")
_ -> IO.puts("Неизвестное сообщение")
after
1000 -> IO.puts("Нет сообщений")
end

Этот код позволяет гибко обрабатывать разные типы сообщений.

Вложенные receive

Можно делать вложенные вызовы receive, но это усложняет отладку. Лучше выносить сложную обработку в отдельные функции.

Практический кейс: обратный вызов

defmodule Echo do
def start do
spawn(fn -> loop() end)
end
defp loop do
receive do
{:echo, msg, sender} ->
send(sender, {:reply, msg})
end
loop()
end
end
pid = Echo.start()
send(pid, {:echo, "тест", self()})
receive do
{:reply, value} -> IO.puts("Ответ: #{value}")
end

Здесь создаётся процесс, который ожидает сообщения {:echo, msg, sender} и отвечает {:reply, msg} обратно.

Особенности

  • send — неблокирующая операция. Если процесс не существует — сообщение теряется.

  • receive — блокирующая. Ожидает сообщения, соответствующие шаблону.

  • Нет общей памяти — все данные передаются копированием.

  • Нет гарантии, что сообщение будет обработано, если не предусмотрен receive.

Пример со стеком сообщений

send(self(), 1)
send(self(), 2)
send(self(), 3)
receive do
2 -> IO.puts("Получено 2")
end

В этом примере сообщения 1 и 3 останутся в очереди, так как receive ждал только 2.

Чтобы вычитать всё — нужно цикл:

def read_all do
receive do
msg ->
IO.inspect(msg)
read_all()
after
100 -> :done
end
end
read_all()

Такой приём позволяет обработать всю очередь.