Erlang (and Elixir) Process Primitives

Santosh Kumar
5 min readMay 17, 2021

Elixir, Erlang and other BEAM languages give you the power of full parallelism.

The core primitive that enables all of this to happen is the spawn BIF (built-in function) provided by OTP.

For example, if you run:

parent = self
:io.format("parent pid ~p~n", [parent])
child_pid = spawn(fn ->
:io.format("child process ~p~n", [self])

:timer.sleep 5_000 # sleep for 5 seconds

:io.format("Child done ~p~n", [self])
send parent, {self, :dead}
end)
receive do
msg -> IO.inspect msg
end

Here we have a parent process that is identified by the PID parent. The parent spawn a child process identified by child_pid.

The child process prints an introductory message to stdout identifying itself. It then proceeds to sleep for 5 seconds and send a message to it’s parent before going away.

The parent in the meanwhile, spawns the child and enters into a loop checking it’s mailbox for any message (this is done using the receive block). Once it gets message, it prints it to stdout (using IO.inspect) before it goes away.

If you understand this code you begin to start to understand how parallelism works on the Erlang Virtual Machine. The basic primitives are send and receive coupled with spawn.

Primitives:
1. spawn — Creates a parallel process and returns a pid that can be used to send messages to this process.
2. send — This is a BIF that allows you to send a message to a process, if you know it’s pid. Example: send(pp, {:hi}) sends a message (the tuple {:hi}) to the process with pid pp.
3. receive — Let’s you pull messages out of your mailbox.

Understanding Erlang Processes

Erlang LWP’s are best understood by diagrams. Here’s one to get started:

Here we have two processes (called A and B) that each have their mailbox that they pull messages from. In order to communicate with a process you send a message to it’s mailbox.

Now, here’s the code that matches what is on the diagram above:

proc_a = spawn(fn ->
:io.format("hello from proc A")
:io.format("Proc A is now going to wait for a message")
receive do
msg ->
IO.puts "proc A received message"
IO.inspect msg
end

IO.puts "Proc A is now ending"
end)
proc_b = spawn(fn ->
:io.format("hello from proc B")
:io.format("Proc Bis now going to wait for a message")
receive do
msg ->
IO.puts "proc Breceived message"
IO.inspect msg
end

IO.puts "Proc B is now ending"
end)

Here we have created ( spawned) the two processes A and B. Each process waits for a message on it’s mailbox, prints the message and then dies.

Process Communication

We would now like to do the following:

In other words, we would like process B to send the message "hi from B" to process A. Or to be more precise, we would like B to put the message "hi from B" in process A's mailbox.

The relevant portions are circled in the image above.

Linking Processes

We often have cases where we would like to create one or more processes whose existence depends on the existence of others.

For example:

Here we have two processes — A and B.

If A dies B has to die, and if B dies A to die. How do we accomplish this?

Erlang has a BIF called link which let’s you set up this contract. Even better, erlang has a BIF called spawn_link that let’s you spawn a process and link to it atomically.

Here’s some code:

proc_a = spawn(fn ->
:io.format("proc A: ~p~n", [self])
Process.register self, :proc_a
proc_b = spawn_link(fn ->
:io.format("proc B: ~p~n", [self])
Process.register self, :proc_b
receive do
end
end)
:io.format("proc A spawn_linked B at: ~p~n", [proc_b]) receive do
end
end)
:timer.sleep 1_000
IO.puts "killing proc B"
proc_b_pid = Process.whereis :proc_b
Process.exit proc_b_pid, :kill
:timer.sleep 100
IO.puts("Proc A: alive? #{Process.alive?(proc_a)}")

Running this you can clearly see the effects of killing B. Proc A is also killed.

Couple of interesting things to note in the above.

The code:

receive do
end

Is a pattern you will see quite a lot in Erlang. It’s how you have a process that waits for a message on it’s mailbox forever and once it get’s it it moves on.

The second thing to note is Process.register. This is a useful function that let’s you register a process with a human readable name instead of a pid.

The third thing to note is that we first created proc A which in turn created B (by calling the spawn_link BIF). After creating B proc A just “hung around”, i.e. kept waiting for a message on it’s mailbox.

Finally, when we killed proc B by calling Process.exit proc_b_pid, :kill and then checked to see if proc A was still alive using calling the Process.alive? function in the final IO.puts we can clearly see that killing B results in A going away as well.

Monitoring Processes

While link'ing processes is great, there are often times when we don’t want both processes to die, if one of them dies. Instead, we want to create one directional link’s where if one of the processes dies the other get’s notified but does NOT die.

This is accomplished by using the spawn_monitor BIF. Consider the following diagram:

Here we would A to monitor B without dying if B dies. How do we do this?

proc_a_pid = spawn_link(fn ->
IO.puts "hello from A"
Process.register self, :proc_a
{mon, proc_b_pid} = spawn_monitor(fn ->
IO.puts "hello from B"
:io.format("procB pid: ~p~n", [self])
Process.register self, :proc_b
receive do
end
end)
receive do
msg ->
IO.puts "proc A received msg"
IO.inspect msg
end
receive do
end
end)
:timer.sleep 500
proc_b_pid = Process.whereis :proc_b
Process.exit proc_b_pid, :kill
:timer.sleep 100
proc_a_alive = Process.alive? proc_a_pid
IO.puts "Proc A alive?: #{proc_a_alive}"

The important piece of code here is:

{mon, proc_b_pid} = spawn_monitor(fn ->

Running this code shows that A received the following message when B (a process it was monitoring) died:

{:DOWN, #Reference<0.4279228456.2173435907.188346>, :process, #PID<0.143.0>,
:killed}

You would also notice that the pid of process B matches that of the :DOWN message received by proc A (this helps in doing pattern matching in the receive block).

Summary

With a rudimentary understanding of these primitives:

  • spawn
  • send
  • receive
  • spawn_link
  • spawn_monitor

You should be in a good place to begin your journey of understanding how Erlang (and other BEAM languages) do concurrency. It’s a journey that will eventually lead you to distributed systems, which is where Erlang really shines. Enjoy!

--

--