Источник Процессы
В Elixir весь код выполняется внутри процессов. Процессы изолированы друг от друга, выполняются параллельно и общаются посредством передачи сообщений. Процессы являются не только основой для конкурентного программирования в Elixir, но и обеспечивают возможность создания распределённых и отказоустойчивых программ.
Процессы Elixir не следует путать с процессами операционной системы. Процессы в Elixir чрезвычайно лёгкие в плане памяти и ЦП (даже по сравнению с потоками, используемыми во многих других языках программирования). По этой причине нередко одновременно работают десятки или даже сотни тысяч процессов.
В этой главе мы изучим основные средства создания новых процессов, а также отправки и получения сообщений между процессами.
Создание процессов
Основным механизмом создания новых процессов является импортируемая по умолчанию функция spawn/1:
iex> spawn(fn -> 1 + 2 end) #PID<0.43.0>
spawn/1 принимает функцию, которая будет выполняться в другом процессе.
Обратите внимание, что spawn/1 возвращает идентификатор процесса (PID). В этот момент созданный процесс, скорее всего, уже завершился. Созданный процесс выполнит заданную функцию и завершится после её выполнения:
iex> pid = spawn(fn -> 1 + 2 end) #PID<0.44.0> iex> Process.alive?(pid) false
Примечание: идентификаторы процессов, которые вы получите, могут отличаться от представленных в этом руководстве.
Мы можем получить PID текущего процесса, вызвав self/0:
iex> self() #PID<0.41.0> iex> Process.alive?(self()) true
Процессы становятся намного интереснее, когда мы можем отправлять и получать сообщения.
Отправка и получение сообщений
Мы можем отправлять сообщения процессу с помощью send/2, а получать их с помощью receive/1:
iex> send(self(), {:hello, "world"})
{:hello, "world"}
iex> receive do
...> {:hello, msg} -> msg
...> {:world, _msg} -> "won't match"
...> end
"world"
Когда сообщение отправляется процессу, оно хранится в почтовом ящике процесса. Блок receive/1 проходит по текущему почтовому ящику процесса, ища сообщение, соответствующее любому из заданных шаблонов. receive/1 поддерживает условия и множество предложений, таких как case/2.
Процесс, который отправляет сообщение, не блокируется при вызове send/2; он помещает сообщение в почтовый ящик получателя и продолжает выполнение. В частности, процесс может отправлять сообщения самому себе.
Если в почтовом ящике нет сообщения, соответствующего какому-либо из шаблонов, текущий процесс будет ожидать поступления такого сообщения. Также можно указать таймаут:
iex> receive do
...> {:hello, msg} -> msg
...> after
...> 1_000 -> "nothing after 1s"
...> end
"nothing after 1s"
Таймаут 0 можно использовать, если вы уже ожидаете, что сообщение находится в почтовом ящике.
Давайте объединим всё вместе и отправим сообщения между процессами:
iex> parent = self()
#PID<0.41.0>
iex> spawn(fn -> send(parent, {:hello, self()}) end)
#PID<0.48.0>
iex> receive do
...> {:hello, pid} -> "Got hello from #{inspect pid}"
...> end
"Got hello from #PID<0.48.0>"
Функция inspect/1 используется для преобразования внутреннего представления структуры данных в строку, обычно для вывода на печать. Обратите внимание, что при выполнении блока receive отправитель процесса, который мы создали, может быть уже завершён, так как его единственной инструкцией было отправление сообщения.
В оболочке может быть полезен вспомогательный метод flush/0. Он очищает и выводит все сообщения из почтового ящика.
iex> send(self(), :hello) :hello iex> flush() :hello :ok
Связи
В большинстве случаев, когда мы создаём процессы в Elixir, мы создаём их как связанные процессы. Перед тем, как показать пример с spawn_link/1, давайте посмотрим, что произойдёт, если процесс, запущенный с помощью spawn/1, завершится ошибкой:
iex> spawn(fn -> raise "oops" end)
#PID<0.58.0>
[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
Была всего лишь записана ошибка, но родительский процесс по-прежнему работает. Это потому, что процессы изолированы. Если мы хотим, чтобы ошибка в одном процессе передавалась в другой, мы должны связать их. Это можно сделать с помощью spawn_link/1:
iex> self()
#PID<0.41.0>
iex> spawn_link(fn -> raise "oops" end)
** (EXIT from #PID<0.41.0>) evaluator process exited with reason: an exception was raised:
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
[error] Process #PID<0.289.0> raised an exception
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
Поскольку процессы связаны, мы теперь видим сообщение, говорящее о том, что родительский процесс, который является процессом оболочки, получил сигнал EXIT от другого процесса, что приводит к завершению оболочки. IEx обнаруживает эту ситуацию и запускает новую сессию оболочки.
Связывание также можно выполнить вручную, вызвав Process.link/1. Рекомендуется ознакомиться с модулем Process для получения дополнительной информации о функциональности процессов.
Процессы и связи играют важную роль при построении отказоустойчивых систем. Процессы Elixir изолированы и по умолчанию ничего не делят. Поэтому ошибка в одном процессе никогда не приведёт к аварийному завершению или порче состояния другого процесса. Однако связи позволяют процессам устанавливать отношения в случае сбоя. Мы часто связываем наши процессы с надсмотрщиками, которые будут обнаруживать, когда процесс умирает, и запускать новый процесс на его месте.
В то время как другие языки потребуют от нас перехватывать/обрабатывать исключения, в Elixir мы можем позволить процессам завершаться ошибкой, поскольку мы ожидаем, что надсмотрщики должным образом перезапустят наши системы. "Быстрое завершение с ошибкой" (иногда называемое "позволить ему завершиться ошибкой") — распространённая философия при написании программного обеспечения на Elixir!
spawn/1 и spawn_link/1 являются основными примитивами для создания процессов в Elixir. Хотя мы использовали их до сих пор, в большинстве случаев мы будем использовать абстракции, построенные на их основе. Давайте рассмотрим наиболее распространённую из них — задачи.
Задачи
Задачи строятся на основе функций spawn, предоставляя более подробные сообщения об ошибках и возможность интроспекции:
iex> Task.start(fn -> raise "oops" end)
{:ok, #PID<0.55.0>}
15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating
** (RuntimeError) oops
(stdlib) erl_eval.erl:668: :erl_eval.do_apply/6
(elixir) lib/task/supervised.ex:85: Task.Supervised.do_apply/2
(stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Function: #Function<20.99386804/0 in :erl_eval.expr/5>
Args: []
Вместо spawn/1 и spawn_link/1 мы используем Task.start/1 и Task.start_link/1, которые возвращают {:ok, pid}, а не только PID. Это то, что позволяет использовать задачи в деревьях надзора. Кроме того, Task предоставляет удобные функции, такие как Task.async/1 и Task.await/1, и функциональность для упрощения распределения.
Мы рассмотрим задачи и другие абстракции, связанные с процессами, в руководстве "Mix и OTP".
Состояние
В этом руководстве мы ещё не обсуждали состояние. Если вы разрабатываете приложение, которому необходимо состояние, например, для хранения конфигурации приложения или для парсинга файла и его хранения в памяти, где вы его сохраните?
Процессы — наиболее распространённый ответ на этот вопрос. Мы можем написать процессы, которые циклически выполняют действия, сохраняют состояние и отправляют и получают сообщения. В качестве примера давайте напишем модуль, который запускает новые процессы, работающие как хранилище ключ-значение в файле с именем kv.exs:
defmodule KV do
def start_link do
Task.start_link(fn -> loop(%{}) end)
end
defp loop(map) do
receive do
{:get, key, caller} ->
send(caller, Map.get(map, key))
loop(map)
{:put, key, value} ->
loop(Map.put(map, key, value))
end
end
end
Обратите внимание, что функция start_link запускает новый процесс, который выполняет функцию loop/1, начиная с пустого отображения. Функция loop/1 (приватная) затем ожидает сообщений и выполняет соответствующее действие для каждого сообщения. Мы сделали loop/1 приватной, используя defp, а не def. В случае сообщения :get она отправляет сообщение обратно вызывающей стороне и снова вызывает loop/1, чтобы ожидать нового сообщения. При сообщении :put фактически вызывается loop/1 с новой версией отображения, с сохранёнными значениями key и value.
Давайте попробуем запустить iex kv.exs:
iex> {:ok, pid} = KV.start_link()
{:ok, #PID<0.62.0>}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
nil
:ok
Сначала отображение процесса не содержит ключей, поэтому отправка сообщения :get и очистка текущего почтового ящика процесса возвращают nil. Давайте отправим сообщение :put и повторим попытку:
iex> send(pid, {:put, :hello, :world})
{:put, :hello, :world}
iex> send(pid, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok
Обратите внимание, как процесс сохраняет состояние, и мы можем получить и обновить это состояние, отправляя сообщения процессу. Фактически, любой процесс, знающий pid выше, сможет отправлять ему сообщения и изменять состояние.
Также можно зарегистрировать pid, присвоив ему имя, и позволить всем, кто знает имя, отправлять ему сообщения:
iex> Process.register(pid, :kv)
true
iex> send(:kv, {:get, :hello, self()})
{:get, :hello, #PID<0.41.0>}
iex> flush()
:world
:ok
Использование процессов для хранения состояния и регистрации имён является распространёнными шаблонами в приложениях Elixir. Однако в большинстве случаев мы не будем реализовывать эти шаблоны вручную, как показано выше, а воспользуемся одной из многочисленных абстракций, поставляемых с Elixir. Например, Elixir предоставляет Agent, которые являются простыми абстракциями вокруг состояния. Наш код выше мог бы быть записан непосредственно как:
iex> {:ok, pid} = Agent.start_link(fn -> %{} end)
{:ok, #PID<0.72.0>}
iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end)
:ok
iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
:world
Параметр :name также может быть задан для Agent.start_link/2, и он будет автоматически зарегистрирован. Помимо агентов, Elixir предоставляет API для создания универсальных серверов (называемых GenServer), реестров и многое другое, всё это основано на процессах. Они, наряду с деревьями надзора, будут изучены подробнее в руководстве "Mix и OTP", где будет построено полное приложение Elixir от начала до конца.
Сейчас перейдём к изучению ввода-вывода в Elixir.
© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/processes.html