Исходный код Процессы
В 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
Поскольку процессы связаны, мы теперь видим сообщение, что родительский процесс, который является оболочкой, получил сигнал завершения от другого процесса, что приводит к завершению оболочки. 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.18.1/processes.html