Spec-Zone.ru › Elixir 1.16

Источник Процессы

В 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.

← Предыдущая страница Перечислимые объекты и потоки
Следующая страница → Ввод-вывод и файловая система

Загрузить версию ePub

Создано с помощью ExDoc (v0.32.2) для языка программирования Elixir

© 2012-2024 The Elixir Team
Licensed under the Apache License, Version 2.0.
https://hexdocs.pm/elixir/1.16.3/processes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API