Mạnh Vũ
Posted on June 13, 2024
One of some difficult to understand from other languages go to Elixir is process (a lightweight/green thread, not OS process) and people usually ignore it to use libraries.
Actually, Elixir process is very powerful thing, If go to work with large scale system we need to do with process a lot.
I have long time work with Erlang (also Golang) and process is one of most interesting thing for me.
From my view, process in Elixir has 3 important things to care.
- Process is isolated code (like an island in ocean) & send/receive message is a way to communicate with outside world. If process is die, it doesn't affect to other processes (except one case is linked process).
- Process can be monitor to know how process is exited (:normal, :error,..).
- Process can be linked (group together) and all processes linked with failed process will be die (except only case is process turn trap_exit to true).
From 3 things above we can made a lot of things with process like: build a custom supervisor/worker model, simple make a pool processes to share workload, make a chain data processing,...
Now we go through one by one for understand Elixir process concept.
Isolated code in process
Work in Elixir much more easy if we understand how it works. From sharing variables (mutable variables) a lit of bit feel hard to bring algorithms (also real loop/while for Elixir). Just imagine you live alone on an island in the ocean and just to one way to communicate with outside by put message to a bottle then ocean will care anything else.
(Actually, we can share state by :ets
, :persistent_term
or another database like :mnesia).
To start new a process we use spawn
, spawn_link
or spawn_monitor
function. An other way to start a process is use Supervisor I will talk about this in other post.
Create a process:
spawn(fn ->
sum = Enum.reduce(1..100, 0, fn n, acc -> acc + n end)
IO.puts "sum: #{inspect sum}"
end)
After spawn (created) process will have a PID
to identify for communicate or control. We can register
a name (an atom) for process.
Register a name for process:
# Register name for current process
Process.register(self(), :my_name)
# Register name for other process.
Process.register(pid, :my_friend)
When you want to send a message you put message with address to bottle then go to beach and throw the bottle to the ocean. With a little bit of time your friend you will receive a message.
And when you want to get a message from other islands you go to the beach and see if has any bottle in the beach, check then get only message you needed (by use pattern matching), other messages stills on the beach.
For action like server/client, you send a message and stay at the beach with time (or forever) to get a feedback (send from island that received your message).
To send in code we use send/2
function:
send(pid, {:get, :user_info, self()})
To receive message (already in mailbox or wait new one) we use receive do
syntax just like case do
:
receive do
{:user_info, data} ->
IO.puts "user data: #{inspect data}"
other ->
IO.puts "other data: #{inspect other}"
end
You can put receive do
to a function to go to loop again if you want to make process like a server.
In case you want to wait with time, you can use after N
with N
is:
0 - Just check existed messages in mailbox, doesn't wait.
0 - Wait in N miliseconds.
:infinity
- Wait forever.
Example:
receive do
{:user_info, data} ->
IO.puts "user data: #{inspect data}"
after 3_000 ->
IO.puts "timeout, nothing for me :("
end
In case message doesn't match any pattern in receive do
it still stay in mailbox of process. You can get it in the future but need sure do not push a lot un matched message to process because it can make a OMM.
link processes
This is very powerful feature of Elixir. We can control a group of processes for do group tasks, chain of tasks and if any of process failed, other processes will die. We don't need take time for clean up that.
Example:
Process A --linked--> B --linked--> C
IO.puts "I'm A"
fun = fn ->
receive do
:shutdown ->
IO.puts "exited"
{:ping, from} ->
IO.puts "got a ping from #{inspect from}"
send(from, :pong)
message ->
IO.puts "Got a message: #{inspect message}"
end
end
# spawn and link process B
spawn_link(fn ->
IO.puts "I'm B"
spawn_link(fn ->
IO.puts "I'm C"
fun.()
end)
fun.()
end)
From this code we link processes in chain task. If any process failed all other processes will die also.
We also make can group like:
Process leader --linked--> worker1, worker2, ..., workerN
And any process is failed all other processes will die follow.
trap_exit
In case we don't want a process go to die follow failed process we can turn trap_exit
on (set flag to true) and process will receive a failed message instead die follow other processes.
Process.flag(:trap_exit, true)
Your process will receive a message like below when linked to your process failed.
{:EXIT, #PID<0.192.0>, {%RuntimeError{message: "#PID<0.192.0>, raise a RuntimeError :D"}, []}}
monitor process
In other case, we just want to now why other process is crashed we can use monitor
process.
Have some function for monitor process: spawn_monitor
, Process.monitor
for make a monitor to other process.
For remove monitor we can use Process.demonitor
.
If monitored process is failed a process make monitor to that process will receive a message like:
{:DOWN, #Reference<...>, :process, #PID<...>, reason}
From link/monitor process we can make a lot of things for our system and we can sleep well! We can make a our specific supervisor, our custom pool workers, chain task, fast fail task,...
In this time I just a explain in basic, I will explain more detail in the future.
I have LiveBook for similar this you can check a and see content + source demo on our Github repo
I will go to details in other posts.
Thank for reading!
Posted on June 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.