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Синхронизацию состояния агента и регистра реализовывать не нужно.
Полезное
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Синхронизацию состояния агента и регистра реализовывать не нужно.
Полезное
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
describe "process register work" do
test "list_registered work" do
{:ok, _} = ProcessRegister.start_link()
assert ProcessRegister.list_registered() == %{}
end
test "add work" do
{:ok, _} = ProcessRegister.start_link()
process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
assert ProcessRegister.add(process, :some_process) == :ok
assert %{some_process: ^process} = ProcessRegister.list_registered()
second_process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
assert ProcessRegister.add(second_process, :other_process) == :ok
assert %{some_process: ^process, other_process: ^second_process} =
ProcessRegister.list_registered()
dead_process = spawn(fn -> 2 + 2 end)
assert ProcessRegister.add(dead_process, :dead) == :ok
assert %{some_process: ^process, other_process: ^second_process} =
ProcessRegister.list_registered()
end
test "drop work" do
{:ok, _} = ProcessRegister.start_link()
process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
ProcessRegister.add(process, :new_process)
assert ProcessRegister.drop(:new_process) == :ok
assert ProcessRegister.list_registered() == %{}
assert ProcessRegister.drop(:not_existing_process) == :ok
end
end
endРешение учителя откроется через:
20:00
