Processes
Erlang/Elixir Processes
All code in Elixir runs in processes. Processes are isolated from each other, run concurrent to one another and communicate via message passing. They provide the means for building distributed and fault-tolerant programs
spawn
The basic mechanism for spawning new processes is the spawn/1
function. It takes a function which it will execute in another process. spawn/1
returns a PID (process identifier)
spawn(fn -> 1 + 2 end)
#PID<0.351.0>
pid = spawn(fn -> 1 + 2 end)
#PID<0.352.0>
Process.alive?(pid)
false
The PID of the current process can be retrieved with the function self/0
self()
#PID<0.349.0>
Process.alive?(self())
true
###
send
and receive
Messages can be sent to a process with send/2
and retrieve them with recieve/1
send(self(), {:hello, "process"})
{:hello, "process"}
receive do
{:hello, msg} -> msg
_ -> IO.puts("unknown message was received")
end
"process"
When a message is sent to a process it is stored in the process mailbox. The recieve/1
block goes through the current process mailbox searching for messages that match any of the given patterns. recieve/1
supports guards and many clauses, such as case/2
If there is no message in the mailbox matching any of the patterns, the current process will wait until a matching message arrives. A timeout can also be specified to avoid long live processes if desired
receive do
{:hello, msg} ->
msg
after
1_000 ->
"nothing after 1 second"
end
"nothing after 1 second"
Putting this all together we can send messages between a process in this Livebook and the Livebook process itself
parent = self()
#PID<0.349.0>
spawn(fn -> send(parent, {:hello, self()}) end)
#PID<0.353.0>
receive do
{:hello, sender} ->
"Received hello from #{inspect(sender)}"
end
"Received hello from #PID<0.353.0>"
Links
The majority of processes spawned in Elixir are “linked”
When a process started with spawn/1
fails, it logs an error but the parent process is still running
spawn(fn -> raise "error!!" end)
#PID<0.354.0>
This is because processes are isolated. If we want the failure in one process to propogate to another one, we need to link them.
This can be done with spawn_link/1
self()
12:07:48.356 [error] Process #PID<0.354.0> on node :"2cy4pr4a-livebook_app@charlie" raised an exception
** (RuntimeError) error!!
/Users/charlie/github.com/charlieroth/lab/elixir/elixir_docs/processes.livemd#cell:2cnmswerwuiaue24ebpzzvsaqmu4pofo:1: (file)
#PID<0.349.0>
The below code will cause all Livebook cells to be “Aborted” so make sure to execute the cell above to get the Livebook back to working order. You can comment out the code in the cell below to continue using the Livebook without interruption.
# spawn_link(fn -> raise "error!" end)
nil
Tasks
Tasks are built on top of spawn functions to provide error reports and introspection
Task.start(fn -> raise "error!" end)
{:ok, #PID<0.418.0>}
12:11:57.351 [error] Task #PID<0.418.0> started from #PID<0.349.0> terminating
** (RuntimeError) error!
/Users/charlie/github.com/charlieroth/lab/elixir/elixir_docs/processes.livemd#cell:v6orldku4babvmh5rnbqyoqz6klvxa5y:1: (file)
(elixir 1.14.2) src/elixir.erl:309: anonymous fn/4 in :elixir.eval_external_handler/1
(elixir 1.14.2) lib/task/supervised.ex:89: Task.Supervised.invoke_mfa/2
Function: #Function<43.3316493/0 in :erl_eval.expr/6>
Args: []
Instead of spawn/1
and spawn_link/1
, Task.start/1
and Task.start_link/1
can be used which return {:ok, pid}
rather than just a PID. Enabling tasks to be used in supervision trees
Tasks provide convience functions like Task.async/1
and Task.await/1
and functionality to ease distribution
State
When building “useful” programs they will most like need to maintain some kind of state (application configuration, parse a file and keep it in memory, etc.)
Processes are the most common answer to this question. State processes can be created by looping infinitely, maintain state in a data structure, send and receive messages
defmodule KV do
def start_link do
Task.start_link(fn -> loop(%{}) end)
end
defp loop(map) do
receive do
{:get, key, from} ->
send(from, Map.get(map, key))
loop(map)
{:put, key, value} ->
Map.put(map, key, value) |> loop()
end
end
end
{:module, KV, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:loop, 1}}
{:ok, kv_pid} = KV.start_link()
{:ok, #PID<0.611.0>}
send(kv_pid, {:get, "hello", self()})
receive do
msg -> msg
end
nil
send(kv_pid, {:put, "hello", "world"})
send(kv_pid, {:get, "hello", self()})
receive do
msg -> msg
end
"world"
send(kv_pid, {:put, "bonjour", "monde"})
send(kv_pid, {:get, "bonjour", self()})
receive do
msg -> msg
end
"monde"
It is also possible to register the pid
with a name and allow other processes to use this name when sending messages
Process.register(kv_pid, :kv)
true
send(:kv, {:put, "goodbye", "see you later"})
send(:kv, {:get, "goodbye", self()})
receive do
msg -> msg
end
"see you later"
Elixir provides an abstraction around a process that maintains state, called an Agent
A :name
option can be given to Agent.start_link/2
and it would automatically be registered
{:ok, ag_pid} = Agent.start_link(fn -> %{} end, name: :double_oh_seven)
{:ok, #PID<0.753.0>}
Agent.update(:double_oh_seven, fn state ->
Map.put(state, :hello, "world")
end)
:ok
Agent.get(:double_oh_seven, fn state ->
state
end)
%{hello: "world"}
Agent.stop(:double_oh_seven)
:ok
Process.alive?(ag_pid)
false