Что такое 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()
Такой приём позволяет обработать всю очередь.