/
Программирование
/
Курс 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

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

Полезное

Команда проекта находится в телеграм-сообществе. Там можно задать любой вопрос и повлиять на проект

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются