While I was learning Elixir basics, I was confused about how to manage process registration and discovery.
I got tons of ideas from the book Elixir in Action by Saša Juric.
Here is my study note on it.
There are multiple ways to manage processes.
1. no registration; remember the pid
- We need to remember the pid that is returned when a genserver process is started.
- We can create as many processes as we want from the same module.
- We need to be aware that a pid will be changed when a process is terminated and recreated.
defmodule MyApp.HelloServer do use GenServer def start_link(id) do GenServer.start_link(__MODULE__, id) end def hello(pid) do GenServer.call(pid, :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> {:ok, pid} = MyApp.HelloServer.start_link(123) {:ok, #PID<0.123.0>} iex> MyApp.HelloServer.hello(pid) "Hello" 2. using module name as local alias
- This is suitable when we need only one process from a module.
- Generally, we just use the module name as a local alias.
defmodule MyApp.HelloServerLocalName do use GenServer def start_link(id) do GenServer.start_link(__MODULE__, id, name: __MODULE__) end def hello do GenServer.call(__MODULE__, :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerLocalName.start_link(123) {:ok, #PID<0.205.0>} iex> MyApp.HelloServerLocalName.hello() "Hello" iex> MyApp.HelloServerLocalName.start_link(123) {:error, {:already_started, #PID<0.205.0>}} 3. using dynamic tuple as local alias (BAD)
When we want to register multiple processes, we may get tempted to generate an atom dynamically (I did); however it is not a good practice.
Erlang has a limit on the number of atoms we can create. Also atoms are not garbage-collected.
defmodule MyApp.HelloServerDynamicName do use GenServer def process_name(id) do String.to_atom("#{__MODULE__}_#{id}") end def start_link(id) do GenServer.start_link(__MODULE__, id, name: process_name(id)) end def hello(id) do GenServer.call(process_name(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerDynamicName.start_link(123) {:ok, #PID<0.164.0>} iex> MyApp.HelloServerDynamicName.hello(123) "Hello" iex> :erlang.system_info(:atom_limit) 1048576 iex> :erlang.system_info(:atom_count) 15849 iex> (1..99) |> Enum.each(fn x -> MyApp.HelloServerDynamicName.start_link(x) end) :ok iex> :erlang.system_info(:atom_count) 16115 4. using Registry and via tuple
- We can use
via_tuplein place of pid. - By using a composite key, many processes can be registered from the same module without creating extra atoms.
- Obviously, a registry process needs to be started before the registration.
defmodule MyApp.ProcessRegistry do def via_tuple(key) when is_tuple(key) do {:via, Registry, {__MODULE__, key}} end def whereis_name(key) when is_tuple(key) do Registry.whereis_name({__MODULE__, key}) end def start_link() do Registry.start_link(keys: :unique, name: __MODULE__) end end defmodule MyApp.HelloServerViaTuple do use GenServer def via_tuple(id) do MyApp.ProcessRegistry.via_tuple({__MODULE__, id}) end def whereis(id) do case MyApp.ProcessRegistry.whereis_name({__MODULE__, id}) do :undefined -> nil pid -> pid end end def start_link(id) do GenServer.start_link(__MODULE__, id, name: via_tuple(id)) end def hello(id) do GenServer.call(via_tuple(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.ProcessRegistry.start_link() {:ok, #PID<0.421.0>} iex> MyApp.HelloServerViaTuple.start_link(123) {:ok, #PID<0.164.0>} iex> MyApp.HelloServerViaTuple.hello(123) "Hello" iex> MyApp.HelloServerViaTuple.whereis(123) #PID<0.164.0> 5. using global alias
- A cluster-wide lock is set so the processes can be shared across multiple nodes.
defmodule MyApp.HelloServerGlobalName do use GenServer def whereis(id) do case :global.whereis_name({__MODULE__, id}) do :undefined -> nil pid -> pid end end def register_process(pid, id) do case :global.register_name({__MODULE__, id}, pid) do :yes -> {:ok, pid} :no -> {:error, {:already_started, pid}} end end def start_link(id) do case whereis(id) do nil -> {:ok, pid} = GenServer.start_link(__MODULE__, id) register_process(pid, id) pid -> {:ok, pid} end end def hello(id) do GenServer.call(whereis(id), :hello) end @impl true def init(id) do {:ok, %{id: id}} end @impl true def handle_call(:hello, _from, state) do {:reply, "hello", state} end end iex> MyApp.HelloServerGlobalName.start_link(123) {:ok, #PID<0.205.0>} iex> MyApp.HelloServerGlobalName.hello(123) "Hello" iex> MyApp.HelloServerGlobalName.start_link(123) {:error, {:already_started, #PID<0.205.0>}} Starting a cluster
- Open two iex shells
- Turn BEAM instances (iex) into nodes
- Make a cluster connecting those notes
iex --sname node1@localhost iex(node2@localhost)> _ iex --sname node2@localhost iex(node2@localhost)> Node.connect(:node1@localhost) true We can confirm processes are shared in two iex shells (BEAM instances).
Finally
With this much info, I feel confident about handling processes. I'll keep on updating the post as soon as I learn something new that is relevant.
Happy coding!

Top comments (0)