Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Processes

elixir/elixir_docs/processes.livemd

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