Spec-Zone.ru › Elixir 1.17

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

В 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.34.1) для языка программирования Elixir

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

Spec-Zone.ru

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