Функция 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
Синхронизацию состояния агента и регистра реализовывать не нужно.
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 describe "process register work" do
5 test "list_registered work" do
6 {:ok, _} = ProcessRegister.start_link()
7
8 assert ProcessRegister.list_registered() == %{}
9 end
10
11 test "add work" do
12 {:ok, _} = ProcessRegister.start_link()
13
14 process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
15
16 assert ProcessRegister.add(process, :some_process) == :ok
17 assert %{some_process: ^process} = ProcessRegister.list_registered()
18
19 second_process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
20
21 assert ProcessRegister.add(second_process, :other_process) == :ok
22
23 assert %{some_process: ^process, other_process: ^second_process} =
24 ProcessRegister.list_registered()
25
26 dead_process = spawn(fn -> 2 + 2 end)
27
28 assert ProcessRegister.add(dead_process, :dead) == :ok
29
30 assert %{some_process: ^process, other_process: ^second_process} =
31 ProcessRegister.list_registered()
32 end
33
34 test "drop work" do
35 {:ok, _} = ProcessRegister.start_link()
36
37 process = spawn(fn -> Process.sleep(:timer.seconds(10)) end)
38
39 ProcessRegister.add(process, :new_process)
40 assert ProcessRegister.drop(:new_process) == :ok
41 assert ProcessRegister.list_registered() == %{}
42
43 assert ProcessRegister.drop(:not_existing_process) == :ok
44 end
45 end
46end
47
Решение учителя откроется через: