Elixir, Erlang and processes
In Erlang and Elixir, process is something entirely different than operating system process. In simplest words: it is a hybrid between thread and and object. Ok, maybe that’s not that simple, let’s have a look at example of simple process:
spawn(fn ->
IO.puts "hello, world!"
:timer.sleep(10000)
end)
This is basically a function, that sleeps for 10 seconds. It is being
started without blocking current process execution, and lives it’s own
life. You probably already know that you can send and receive messages
from processes. OTP comes with a handy behaviors, such as GenServer
that allows us to implement processes that respond to various messages
and store state.
We can do all, provided we know the PID of the process. This is the case when we started the process ourselves. But quite often, we want our OTP application to initialize the supervision tree of processes for us, from our application callback module.
lib/project.ex
:
defmodule Project do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
supervisor(Project.Endpoint, []),
supervisor(Project.Repo, []),
worker(Project.Worker, []),
]
opts = [strategy: :one_for_one, name: Project.Supervisor]
Supervisor.start_link(children, opts)
end
end
How do we send messages to our Project.Worker
if we are not the ones
that start it in the first place? The answer to that problem is name
registration.
Naming processes
Elixir and Erlang allow us to name processes. By giving process name, we associate PID with certain atom.
Let’s have a look at our worker:
lib/worker.ex
:
defmodule Project.Worker do
use GenServer
def start_link(_ignore \\ nil) do
GenServer.start_link(__MODULE__, nil, [])
end
def handle_call("Hi!", _from, state) do
{:reply, "Hola!", state}
end
end
We can spawn such worker and communicate with it using it’s PID:
{:ok, pid} = Project.Worker.start_link
{:ok, #PID<0.97.0>}
iex(4)> GenServer.call(pid, "Hi!")
"Hola!"
But if if this worker is started by our application’s callback module, inside a supervision tree, how do we find it?
Elixir allows us to do simple name registration for our processes. We can do it manually:
iex(1)> {:ok, pid} = Project.Worker.start_link
{:ok, #PID<0.93.0>}
iex(2)> Process.register pid, Project.Worker
true
iex(3)> pid = Process.whereis(Project.Worker)
#PID<0.93.0>
iex(4)> GenServer.call(pid, "Hi!")
"Hola!"
And that works great. But OTP behaviors allow us to do it even simpler, by passing name in place of PID in various places:
iex(5)> GenServer.call(Project.Worker, "Hi!")
"Hola!"
Sweet.
Note: Project.Worker
is an atom. In Elixir module names are atoms. It is
a common pattern to name processes that have only instance of given
module by it’s module name, but we do not have to. We can register our
processes under any atom, but a process can have only one name:
iex(1)> {:ok, pid} = Project.Worker.start_link
{:ok, #PID<0.93.0>}
iex(2)> Process.register pid, :some_atom
true
iex(3)> Process.register pid, :some_other_atom
** (ArgumentError) argument error
:erlang.register(:some_other_atom, #PID<0.93.0>)
(elixir) lib/process.ex:338: Process.register/2
You can’t register two processes under the same name either:
iex(1)> {:ok, pid1} = Project.Worker.start_link
{:ok, #PID<0.93.0>}
iex(2)> {:ok, pid2} = Project.Worker.start_link
{:ok, #PID<0.95.0>}
iex(3)> Process.register pid1, Project.Worker
true
iex(4)> Process.register pid2, Project.Worker
** (ArgumentError) argument error
:erlang.register(Project.Worker, #PID<0.95.0>)
(elixir) lib/process.ex:338: Process.register/2
OTP makes it easier
When we let our supervisors manage our processes, we do not need to
manually Process.register
each of the processes. The functionality is
built into OTP. We need to allow our worker to register it’s name on
start up:
defmodule Project.Worker do
use GenServer
def start_link(name \\ nil) do
GenServer.start_link(__MODULE__, nil, [name: name])
end
def handle_call("Hi!", _from, state) do
{:reply, "Hola!", state}
end
end
And then spawn a named worker from our application callback module:
defmodule Project do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(Project.Worker, [Project.Worker]),
]
opts = [strategy: :one_for_one, name: Project.Supervisor]
Supervisor.start_link(children, opts)
end
end
This is enough to find our process by it’s name and call it:
iex(1)> GenServer.call Project.Worker, "Hi!"
"Hola!"
Of course this way you can start multiple workers, such as:
defmodule Project do
use Application
def start(_type, _args) do
import Supervisor.Spec, warn: false
children = [
worker(Project.Worker, [Project.Worker1]),
worker(Project.Worker, [Project.Worker2]),
worker(Project.Worker, [Project.Worker3]),
]
opts = [strategy: :one_for_one, name: Project.Supervisor]
Supervisor.start_link(children, opts)
end
end
…but this becomes inconvenient as soon as you want to dynamically
spawn and shut down your workers. For such complex behaviors, you should
either use built in :pg2
process registry, :glob
global process
registry or very powerful and flexible :grpoc.
This is it for now, more about alternative ways to register processes soon! Stay tuned for next blog posts on the subject!
Post by Hubert Łępicki
Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.
RL6416FJTSAP https://dzen.ru sgjvhnbtcbtfgdjgfb
R43HJ2BX https://dzen.ru sgjvhnbtcbstfgdjgfbs