Spec-Zone.ru › Elixir 1.18

Исходный код Процессы

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

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

Скачать версию ePub

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

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

Spec-Zone.ru

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