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
Синхронизацию состояния агента и регистра реализовывать не нужно.
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.