Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

Elixir: Регистрация процессов

Функция spawn, при вызове, возвращает идентификатор процесса:

spawn(fn -> 2 + 2 end)
# => #PID<0.197.0>

Однако, если процесс создается не напрямую, например через супервизора, то у программиста "пропадает" доступ к идентификатору процесса. Для таких случаев используется регистрация процессов. Вспомним код агента-счетчика из прошлых упражнений:

defmodule Counter do
  use Agent

  def start_link(initial_state \\ 0) do
    Agent.start_link(fn -> initial_state end, name: __MODULE__)
  end

  def current_value, do: Agent.get(__MODULE__, fn state -> state end)
  def inc, do: Agent.update(__MODULE__, fn state -> state + 1 end)
  def dec, do: Agent.update(__MODULE__, fn state -> state - 1 end)
end

При вызове функции Agent.start_link, передается опция :name, в которой находится название нынешнего модуля, то есть Counter. В итоге после создания, процесс сохраняется в локальном регистре процессов под названием модуля Counter:

Counter.start_link()
# => {:ok, #PID<0.115.0>}

А теперь проверим список зарегистрированных процессов:

Process.registered()
# => [:user_drv, :rex, :inet_db, :elixir_code_server, :erl_prim_loader,
# => :user_drv_writer, :global_name_server, :init, :kernel_sup, :code_server,
# => :erts_code_purger, :logger_std_h_default, :logger_sup, :socket_registry,
# => IEx.Config, :user_drv_reader, :erl_signal_server, :elixir_config, IEx.Pry,
# => :file_server_2, :standard_error_sup, Logger.Supervisor, :global_group,
# => :application_controller, :kernel_safe_sup, :user, :logger_proxy, IEx.Broker,
# => :logger, :global_group_check, Counter, IEx.Supervisor, :kernel_refc,
# => :elixir_sup, :logger_handler_watcher, :standard_error]

Как видно из списка, при запуске консоли по умолчанию регистрируется множество разных процессов, но помимо них, в списке появился Counter. Благодаря этому, мы теперь можем обращаться к процессу через название модуля:

# найдем процесс
pid = Process.whereis(Counter)
# => #PID<0.115.0>

# обновим состояние агента напрямую
Agent.update(pid, fn state -> state + 100 end)
# => :ok
Counter.current_value()
# => 100

В примере выше, не стоит обновлять состояние агента напрямую через идентификатор процесса, так как это обход абстракций. Модуль представляет отдельные функции для работы с ним, которые скрывают детали реализации, поэтому ими и нужно пользоваться. А теперь попробуем еще раз запустить процесс-счетчик:

Counter.start_link()
# => {:error, {:already_started, #PID<0.115.0>}}

Процесс уже запущен и зарегистрирован, поэтому повторное создание не требуется. Попробуем убить процесс и сделаем пару проверок:

pid = Process.whereis(Counter)
Process.exit(pid, :kill)
# => ** (EXIT from #PID<0.109.0>) shell process exited with reason: killed
# => Interactive Elixir (1.15.5) - press Ctrl+C to exit (type h() ENTER for help)

Process.registered()
# => [:user_drv, :rex, :inet_db, :elixir_code_server, :erl_prim_loader,
# => :user_drv_writer, :global_name_server, :init, :kernel_sup, :code_server,
# => :erts_code_purger, :logger_std_h_default, :logger_sup, :socket_registry,
# => IEx.Config, :user_drv_reader, :erl_signal_server, :elixir_config, IEx.Pry,
# => :file_server_2, :standard_error_sup, Logger.Supervisor, :global_group,
# => :application_controller, :kernel_safe_sup, :user, :logger_proxy, IEx.Broker,
# => :logger, :global_group_check, IEx.Supervisor, :kernel_refc, :elixir_sup,
# => :logger_handler_watcher, :standard_error]

Counter.current_value()
# => ** (exit) exited in: GenServer.call(Counter, {:get, #Function<0.123767520/1 in Counter.current_value/0>}, 5000)
# =>   ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
# =>   (elixir 1.15.5) lib/gen_server.ex:1063: GenServer.call/3
# =>   iex:39: (file)

Process.whereis(Counter)
# => nil

Сначала мы отправили сигнал агенту о завершении, но так как процесс агента был связан напрямую с процессом интерактивной оболочкой, при аварийном завершении процесса агента заодно завершился процесс оболочки. Произошло это из-за того, что процесс оболочки в данном случае выступал "супервизором" относительно процесса счетчика. Однако, из-за того, что процесс оболочки тоже находится под наблюдением другого супервизора, то при аварийном завершении оболочки, супервизор перехватил ошибку и перезапустил процесс интерактивной оболочки, но не перезапустил процесс счетчика. Счетчик не был перезапущен ровно потому, что о нем было известно только процессу оболочки и никаких стратегий перезапуска для счетчика не было указано при старте интерактивной оболочки. Ровно об этом и говорит код ниже, что счетчика, как процесса, больше не существует, его нет в регистре процессов и его нельзя менять.

В базовом случае, процессы регистрируются через Process.register, однако процесс при регистрации должен быть обязательно живым:

# долгоживущий процесс
pid = spawn(fn -> Process.sleep(:timer.seconds(20)) end)
#PID<0.118.0>

Process.register(pid, :my_process)
# => true

Process.registered()
# => [..., :my_process, ...]

# спустя 20 секунд
Process.registered()
# => [...] процесса с именем :my_process нет

# попробуем записать быстроживущий процесс
pid = spawn(fn -> 2 + 2 end)
# => #PID<0.119.0>

Process.register(pid, :other_process)
# => ** (ArgumentError) could not register #PID<0.119.0> with name :other_process because it is not alive, the name is already taken, or it has already been given another name
# =>     (elixir 1.15.5) lib/process.ex:698: Process.register(#PID<0.119.0>, :other_process)
# =>     iex:42: (file)

В целом это все что необходимо знать о регистрации процессов. Если нужно регистрировать несколько одинаковых процессов под разными именами, то нужно к этому подходить с осторожностью. Например, не генерировать атомы, так как они не вычищаются сборщиком мусора и в один момент приложение переполнится по памяти.

Если стандартного регистра процессов не хватает, то можно сделать свой, указав, к примеру, каким образом разрешать конфликты имен и как в целом хранить информацию о зарегистрированных процессах. Об этом подробнее можно почитать уже в официальной документации.

Задание

Допишите агента, добавив функции add, drop для регистрации/снятия процессов с учета и функцию list_registered, которая выводит список процессов, которые зарегистрированы через агента. Если процесс завершился, то при вызове add регистрировать процесс не нужно. Если удаляемый процесс не существует, то нужно вернуть :ok, учтите, что Process.unregister вызывает исключение при попытке удалить несуществующий процесс.

ProcessRegister.start_link()
# => {:ok, #PID<0.139.0>}

ProcessRegister.list_registered()
# => %{}

pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end)
ProcessRegister.add(pid, :timeout_process)
# => :ok
pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end)
ProcessRegister.add(pid, :other_timeout)
# => :ok
pid = spawn(fn -> Process.sleep(:timer.seconds(30)) end)
ProcessRegister.add(pid, :timeout)
# => :ok

ProcessRegister.list_registered()
# => %{
# =>   timeout: #PID<0.152.0>,
# =>   timeout_process: #PID<0.150.0>,
# =>   other_timeout: #PID<0.151.0>
# => }

Process.registered()
# => [..., :timeout, :timeout_process, :other_timeout, ...]

ProcessRegister.drop(:timeout_process)
# => :ok
ProcessRegister.list_registered()
# => %{
# =>   timeout: #PID<0.152.0>,
# =>   other_timeout: #PID<0.151.0>
# => }

Process.registered()
# => [..., :timeout, :other_timeout, ...]

  ProcessRegister.drop(:not_existing_process)
# => :ok

Синхронизацию состояния агента и регистра реализовывать не нужно.

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе