Elixir Processes & The Actor Model

Elixir is a functional language, but programs are rarely structured around simple functions. Instead, Elixir’s key organizational concept is the process, an independent component (built from functions) that sends and receives messages. Programs are deployed as sets of processes that communicate with each other. The context of each process is isolated from the context of other processes. Messages are required to share information between the processes.

This is considered the actor model. Each process is an actor, capable of sending and receiving messages from other actors. In response to a message that it receives, an actor can: make local decisions, create more actors, send more messages, and determine how to respond to the next message received.

This approach makes it much easier to distribute work across multiple processors or computers, and also makes it possible to do things like upgrade programs in place without shutting down the whole system.

Process Identifiers (pid)

Every process gets its own unique pid, think of it as the address for the mailbox of a process. Your programs will send messages from one process to another by sending them to a pid. When that process gets time to check its mailbox, it will be able to retrieve and process the messages there. Pids can even identify processes running on multiple computers within a cluster.

Self

The helper function self/0 returns the the identity of the current process. When running this in IEX, it will be the REPL process.

iex()> self()
#PID<0.60.0>

The three numbers in the identifier are the process address. They give us the full address to any process. We can use this reference identifier as an address to send messages.

Sending messages

We can send messages to our own inbox by using self(). Here we send the message :hello to our own process inbox.

iex()> send self(), :hello

Receiving messages

We can use the flush/0 helper function to dump the content of the current process mailbox:

iex()> flush()
:hello
:ok

This is nice when working in the REPL, but the proper way to read messages is to use the receive...end construct. Here we can pattern match for specific messages. In this case we just get whatever is in our inbox.

iex()> receive do
...()>   x -> x
...()> end
:hello

Starting processes

Let’s create a worker process that we can spawn, and then send some work to it for it to perform. This worker will pattern match for messages that look like this: {:square, numbers, pid}, a triple consisting of an action :square, a list of numbers to be squared and finally a pid, where to send the result.

Create a file called worker.exs and type in / paste the following content:

defmodule Worker do
  def do_work() do
    receive do
      {:square, numbers, pid} ->
        send pid, {:result, square(numbers)}
    end
    do_work()
  end

  defp square(numbers) do
    numbers 
    |> Enum.map(&(&1 * &1))
  end
end

Notice the do_work function, it checks the mailbox for incomming messages and then calls itself recursively. This is a common pattern with Elixir and processes. The only job this process/actor has is to listen for incomming work and send the result.

Import the script we created

iex()> import_file "worker.exs"

Now lets use the spawn/3 function to start a worker process. It returns the pid of the spawned worker process.

iex()> pid = spawn Worker, :do_work, []

Next, we send a message with some numbers to be squared, and the pid of our current process (the REPL) by using the self() function to get the address of our inbox.

iex()> send pid, {:square, [1, 2, 3, 4, 5], self()}
{:square, [1, 2, 3, 4, 5], #PID<0.81.0>}

Now let’s check our inbox:

iex()> receive do x -> x end
{:result, [1, 4, 9, 16, 25]}

We can also check that the worker is still up and running:

iex()> Process.alive?(pid)
true

Let’s break things

What happens if the spawned process crashes? Let’s try breaking it.

iex()> send pid, {:square, ["square", "pants"], self()}
10:14:01.332 [error] Process #PID<0.162.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    iex:14: anonymous fn/1 in Worker.square/1
    (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
    iex:5: Worker.do_work/0

The invalid message causes the worker to crash and burn

iex()> Process.alive?(pid)
false

The process has crashed because it cannot square strings. We want to be notified that the process exited so that we know not to attempt to send messages to it, or resurrect it before sending more messages. We can use spawn_link/3 for this.

Spawning with links

Process links create relationships between the processes and enable another channel of communication to be used between the two. The channel allows processes to be signaled when another process dies.

Process links serve the purpose of enabling the cascading failure of dependent or downstream processes when an upstream process failes.

iex()> pid = spawn_link Worker, :do_work, []
iex()> send pid, {:square, ["hello", "world"], self()}
16:33:59.055 [error] Process #PID<0.189.0> raised an exception
** (ArithmeticError) bad argument in arithmetic expression
    iex:14: anonymous fn/1 in Worker.square/1
    (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
    iex:5: Worker.do_work/0
** (EXIT from #PID<0.171.0>) an exception was raised:
    ** (ArithmeticError) bad argument in arithmetic expression
        iex:14: anonymous fn/1 in Worker.square/1
        (elixir) lib/enum.ex:1229: Enum."-map/2-lists^map/1-0-"/2
        iex:5: Worker.do_work/0

As you can se, the REPL process exits, and we get a new one:

Interactive Elixir (1.4.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

A linked process does not always kill the parent, however. The propagation of the errors is related to the reason the child process is terminated.

We can create a more robust version of our worker process to also receive the exit signals.

defmodule Worker do
  def do_work() do
    receive do
      {:square, numbers, pid} ->
        send pid, {:result, square(numbers)}
      {:exit, reason} ->
        exit(reason)
    end
    do_work()
  end

  defp square(numbers) do
    numbers 
    |> Enum.map(&(&1 * &1))
  end
end

Reload the script.

iex()> import_file "worker.exs"
iex()> pid = spawn_link Worker, :do_work, []
iex()> Process.alive?(pid)
true

We can send an exit signal that will terminate the worker process, leaving the parent process unaffected.

iex()> send pid, {:exit, :normal}
iex()> Process.alive?(pid)
false

There are two standard exit reasons in Elixir, :normal and :kill

References