Understanding the basics of Elixir's concurrency model
Table of Contents
If you come from an object-oriented background, you might have tried concurrency in your favourite OO language before. Your mileage will vary, but in general OO languages are harder to work with when it comes to concurrency. This is due to their nature - they are designed to keep state in memory and require more expertise and experience to be successful with.
How does Elixir stand up to other languages when it comes to concurrency? Well, for starters just being functional and immutable is a big win - no state to manage. But also, Elixir has more goodies packing under the hood and in its standard library.
Being based on Erlang’s virtual machine (a.k.a. the BEAM), Elixir uses processes to run any and all code. Note that from here on any time we mention processes, unless specified otherwise, we are referring to BEAM processes, not OS processes.
Elixir’s processes are isolated from one another, they do not share any memory and run concurrently. They are very lightweight and the BEAM is capable of running many thousands of them at the same time. That’s why Elixir exposes primitives for creating processes, communicating between them and various modules on process management.
Let’s see how we can create processes and send messages between them.
Creating processes #
One of the simplest (yet powerful) tools in Elixir’s toolbelt is IEx
-
Elixir’s REPL, short for Interactive Elixir. If we jump into IEx
and run h self
, we will get the following output:
iex(1)> h self
def self()
@spec self() :: pid()
Returns the PID (process identifier) of the calling process.
Allowed in guard clauses. Inlined by the compiler.
As you can see, self()
is a built-in function in Elixir which returns the PID
(process identifier) of the calling process. If you run it in IEx
it will
return the PID of IEx
:
iex(2)> self
#PID<0.102.0>
Now, just like IEx
is a BEAM process, with its own PID, it can also create
new processes. In fact, any BEAM process can spawn
BEAM processes. This is
done using the Kernel.spawn/1
function:
iex(3)> spawn(fn -> 1 + 1 end)
#PID<0.108.0>
spawn/1
creates a new process which invokes the function provided as
argument, fn -> 1 + 1 end
. What you might notice is that we do not see the
return value of the anonymous function because the function ran in a different
process. What we get instead is the PID of the spawned process.
Another thing worth noticing is that once we spawn
a process it will run
right away, which means that the function will be immediately executed. We can
check that using the Process.alive?/1
function:
iex(4)> pid = spawn(fn -> 1 + 1 end)
#PID<0.110.0>
iex(5)> Process.alive?(pid)
false
While we don’t need to look at the Process
module in depth right now, it has
quite a bit more functions available for working with processes. You can explore
its documentation here.
Now, let’s look at receiving messages.
Receiving messages in processes #
For the purpose of our example, let’s imagine we open a new IEx
session and
we tell the IEx
process (a.k.a. self
) that it might receive a message that
it should react to. If this makes you scratch your head a bit, remember that
since IEx
is a BEAM process it can receive messages just like any other BEAM
process.
The messages that we would like our IEx
process to receive will be:
- tuple containing
:hello
atom and a string with a name (e.g.{:hello, "Jane"}
) - tuple containing the
:bye
atom and a string with a name (e.g.{:bye, "John"}
)
When the process receives the message in the first case it should reply with
“Hello Jane”, while in the second case it should reply with “Bye John”. If we
could use the cond
macro in Elixir, it would look something like:
cond message do
{:hello, name} -> "Hello #{name}"
{:bye, name} -> "Bye #{name}"
end
To receive messages in a process we cannot use cond
, but Elixir provides us
with a function receive/1
which takes a block as an argument. Although it’s
not cond
, it looks very similar to the example above because it lets us use
pattern-matching:
receive do
{:hello, name} -> "Hello #{name}"
{:bye, name} -> "Bye #{name}"
end
What you’re seeing here usage of BEAM’s actor model for concurrency in Elixir. If you’re not familiar with the model worry not - you can read an interesting ELI5 about it here on Dev.to, or you can just keep on reading and by the end of this article you should have a good idea about it.
The receive
function takes the received message and tries to pattern-match it
to one of the statements in the block. Obviously, it accepts not just a value
to return but also a function call. As you can imagine, receive/1
is what is
called when the mailbox of the process gets a new message in.
Now that we fixed up the actions on the mailbox of our process, how can we send
a message to our IEx
process?
This is done via the send/2
function. The function takes the PID and the
message itself as arguments. For our example, let’s send the following message
to our IEx
process:
iex(1)> send self(), {:hello, "John"}
{:hello, "John"}
What you see here is the message being dropped in IEx
’s mailbox. This means
that we need to invoke the receive
function in our IEx
session so we
can process the messages in the mailbox:
iex(2)> receive do
...(2)> {:hello, name} -> "Hello #{name}"
...(2)> {:bye, name} -> "Bye #{name}"
...(2)> end
"Hello John"
Right after executing the receive
block the message will be immediately
processed and we will see "Hello John"
returned.
What if we never receive a message? #
One thing to notice here is that if we would just write the receive
block in
our IEx
session it would block until the process receives a message. That’s
expected - if there is no message in the mailbox matching any of the patterns,
the current process will wait until a matching message arrives. This is the
default behaviour of receive/1
.
Obviously, if we get ourselves stuck in such way we can always stop the IEx
session by using Ctrl+C
. But, what if a process in our application gets
stuck? How can we tell it that it should stop waiting after a certain amount
of time if it does not receive a message?
One nicety that Elixir provides us with is setting a timeout using after
:
iex(6)> receive do
...(6)> {:hello, name} -> "Hello #{name}"
...(6)> {:bye, name} -> "Bye #{name}"
...(6)> after
...(6)> 1_000 -> "Nothing after 1s"
...(6)> end
"Nothing after 1s"
What happens here is that the timeout function is executed after 1000
milliseconds pass and the receive/1
function exits (hence stops blocking).
This prevents processes hanging around waiting forever for a matching message
to arrive in their mailboxes.
Long-running processes #
So far we were looking at sending and receiving messages to the same process -
the IEx
process itself. This is quite a simple example and won’t help when we
would like to put processes into action in a production application.
At this point, you might be wondering how to actually spawn a process that
would react to multiple incoming messages, instead of just (ab)using the IEx
process with the receive/1
function.
Well, to do this we have to make a process run infinitely (or until we ask it to die). How? By creating an infinite loop in the process itself.
WTF you mean by “an infinite loop”?! #
Yeah, I know, it feels weird, doesn’t it? Here’s the thing - the BEAM has an optimisation in it, so-called a last-call optimisation (read Joe Armstrong’s explanation of this optimisation here), where if the last call in a function is a call to the same function, Elixir will not allocate a new stack frame on the call stack. In fact, it will just jump to the beginning of the same function (instead of running another instance of it), which will prevent a stack overflow happening to our program.
This means that it’s virtually impossible to smash the stack in the BEAM languages (Erlang & Elixir), if we are just a bit careful when composing these self-invoking functions.
Long-running processes, continued #
Let’s look at a small module with a single function:
defmodule MyProcess do
def start do
receive do
{:hello, name} ->
IO.puts "Hello #{name}!"
start()
{:bye, name} ->
IO.puts "Bye #{name}. Shutdown in 3, 2, 1..."
end
end
end
The MyProcess.start
function when run will wait for a message to arrive in
the process’ mailbox. Then, it will try to pattern match on the arrived message
and execute the associated code. One trick is that at the end of the first case
we execute the start
function again, which will create an infinite loop in
the process, therefore having the process waiting for messages forever.
Let’s look how this will work in IEx
:
iex(1)> pid = spawn(MyProcess, :start, [])
#PID<0.120.0>
iex(2)> send pid, {:hello, "Ilija"}
Hello Ilija!
{:hello, "Ilija"}
iex(3)> send pid, {:hello, "Jane"}
Hello Jane!
{:hello, "Jane"}
iex(4)> send pid, {:bye, "Jane"}
Bye Jane. Shutdown in 3, 2, 1...
{:bye, "Jane"}
First, we use spawn/3
to create a process that will run the MyProcess.run
function. spawn/3
is a flavour of spawn/1
- the only difference is that
spawn/3
knows how to run a named function in a process, while spawn/1
takes
only anonymous functions as arguments.
Then, you can see that every time we send a {:hello, "Name"}
message to the
process (using send/2
), we see the process printing back the greeting. Once
we send the {:bye, "Jane"}
message the process prints that it’s shutting
down.
How so? Well, if you look at the MyProcess.start
function you will notice
that it does not invoke itself after it prints out the shutdown message. This
means that once it handles that message the MyProcess.start
function will
finish and the process will die.
Let’s test that in IEx
, using the Process.alive?/1
function:
iex(1)> pid = spawn MyProcess, :start, []
#PID<0.109.0>
iex(2)> Process.alive? pid
true
iex(3)> send pid, {:hello, "Ilija"}
Hello Ilija!
{:hello, "Ilija"}
iex(4)> Process.alive? pid
true
iex(5)> send pid, {:bye, "Ilija"}
Bye Ilija. Shutdown in 3, 2, 1...
{:bye, "Ilija"}
iex(6)> Process.alive? pid
false
Now that we know how to send and receive multiple messages to a process in Elixir, you might be wondering what is a good use-case to use processes for?
The answer is: keeping state.
Keeping state using processes #
You might find this weird, but this is a classic example to keeping state in Elixir. While our example here will not take care of storing state on disk, cover all edge-cases that might occur or be a bullet-proof solution, I hope it will get your brain juices flowing on processes and how to use them.
Let’s write a Store
module that will have a start
function. It should spawn
a process which will invoke a function of the same module, for now,
called foo
:
# store.exs
defmodule Store do
def start do
spawn(__MODULE__, :foo, [%{}])
end
def foo(map) do
IO.puts "Nothing so far"
end
end
Let’s briefly dissect the Store.start
function: what it does is it spawn
s a
new process calling the foo/1
function and it passes an empty map (%{}
) as an
argument to the function. If the special keyword __MODULE__
throws you off,
it’s just an alias to the module name:
iex(1)> defmodule Test do
...(1)> def eql?, do: __MODULE__ == Test
...(1)> end
iex(2)> Test.eql?
true
This means that when we call Store.start
we will immediately see the output of
the function in the process. Right after, the process will die:
iex(4)> pid = Store.start
Nothing so far...
#PID<0.117.0>
iex(5)> Process.alive? pid
false
This means that we need to make foo/1
a function that will loop forever. Or at
least until we tell it to stop.
Let’s rename foo/1
to loop/1
and make it loop forever:
defmodule Store do
def start do
spawn(__MODULE__, :loop, [%{}])
end
def loop(state) do
receive do
loop(state)
end
end
end
If we run this module now in an IEx
session the process will work forever.
Instead of doing that, let’s add a special “system” message that we can send to
the process so we can force it to shut down:
defmodule Store do
def start do
spawn(__MODULE__, :loop, [%{}])
end
def loop(state) do
receive do
{:stop, caller_pid} ->
send caller_pid, "Shutting down"
_ ->
loop(state)
end
end
end
So now, you see that when there’s no match, the Store.loop/1
function will
just recurse, but when the :stop
message is received it will just send
a
"Shutting down"
message to the calling PID.
iex(1)> pid = Store.start
#PID<0.125.0>
iex(2)> send pid, {:stop, self()}
{:stop, #PID<0.102.0>}
iex(3)> flush()
"Shutting down."
:ok
What you’re seeing here is a very simple example of sending messages between
two processes - the Store
process and our IEx
session process. When we send
the :stop
message we also send the PID of the IEx
session (self
), which
is then used by Store.loop/1
to send the reply back. At the end, instead of
writing a whole receive
block for the IEx
session we just invoke flush
,
which flushes the IEx
process mailbox and returns all of the messages in the
mailbox at that time.
If you’re feeling deep in the rabbit hole now worry not - we are going to address keeping state in a process right away!
Let’s say that our Store
will accept four commands:
stop
- the one we already implemented, which stops theStore
processput
- adds a new key-value pair to thestate
mapget
- fetches a value for a given key from thestate
mapget_all
- fetches all of the key-value pairs that are stored in thestate
map
Putting a value #
Let’s implement the put
command:
defmodule Store do
def start do
spawn(__MODULE__, :loop, [%{}])
end
def loop(state) do
receive do
{:put, key, value} ->
new_state = Map.put(state, key, value)
loop(new_state)
{:stop, caller} ->
send caller, "Shutting down."
_ ->
loop(state)
end
end
end
When we send a tuple containing {:put, :foo, :bar}
to the process, it will
add the :foo => :bar
pair to the state
map. The key here is that it will
invoke State.loop/1
again with the updated state (new_state
). This will make
sure that the key-value pair we added will be included in the new state on the
next recursion of the function.
Getting values #
Let’s implement get
so we can test get
and put
together via an IEx
session:
defmodule Store do
def start do
spawn(__MODULE__, :loop, [%{}])
end
def loop(state) do
receive do
{:stop, caller} ->
send caller, "Shutting down."
{:put, key, value} ->
new_state = Map.put(state, key, value)
loop(new_state)
{:get, key, caller} ->
send caller, Map.fetch(state, key)
loop(state)
_ ->
loop(state)
end
end
end
Just like with the other commands, there’s no magic around get
. We use
Map.fetch/2
to get the value for the key passed. Also, we take the PID of
the caller
so we can send back to the caller the value found in the map:
iex(1)> pid = Store.start
#PID<0.119.0>
iex(2)> send pid, {:put, :name, "Ilija"}
{:put, :name, "Ilija"}
iex(3)> send pid, {:get, :name, self()}
{:get, :name, #PID<0.102.0>}
iex(4)> flush
{:ok, "Ilija"}
:ok
iex(5)> send pid, {:put, :surname, "Eftimov"}
{:put, :surname, "Eftimov"}
iex(6)> send pid, {:get, :surname, self()}
{:get, :surname, #PID<0.102.0>}
iex(7)> flush
{:ok, "Eftimov"}
:ok
If we look at the “conversation” we have with the Store
process, at the
beginning, we set a :name
key with the value "Ilija"
and we retrieve it
after (and we see the reply using flush
). Then we do the same exercise by
adding a new key to the map in the Store
, this time :surname
with the value
"Eftimov"
.
From the “conversation” perspective, the key piece here is us sending self()
-
the PID of our current process (the IEx
session) - so the Store
process knows
where to send the reply to.
Getting a dump of the store #
Right before we started writing the Store
module we mentioned that we will
also implement a get_all
command, which will return all of the contents of the
Store
. Let’s do that:
defmodule Store do
def start do
spawn(__MODULE__, :loop, [%{}])
end
def loop(state) do
receive do
{:stop, caller} ->
send caller, "Shutting down."
{:put, key, value} ->
new_state = Map.put(state, key, value)
loop(new_state)
{:get, key, caller} ->
send caller, Map.fetch(state, key)
loop(state)
{:get_all, caller } ->
send caller, state
loop(state)
_ ->
loop(state)
end
end
end
If you expected something special here, I am very sorry to disappoint you. The
implementation of the get_all
command is to return the whole state
map of
the process to the sender.
Let’s test it out:
iex(1)> pid = Store.start
#PID<0.136.0>
iex(2)> send pid, {:put, :name, "Jane"}
{:put, :name, "Jane"}
iex(3)> send pid, {:put, :surname, "Doe"}
{:put, :surname, "Doe"}
iex(4)> send pid, {:get_all, self()}
{:get_all, #PID<0.102.0>}
iex(5)> flush
%{name: "Jane", surname: "Doe"}
:ok
As expected, once we add two key-value pairs to the Store
, when we invoke the
get_all
command the Store
process sends back the whole state
map.
While this is a very small and contrived example, the skeleton we followed here by keeping state by using recursion, sending commands and replies back and forth to the calling process is actually used quite a bit in Erlang and Elixir.
A small disappointment #
First, I am quite happy you managed to get to the end of this article. I believe that once I understood the basics of the concurrency model of Elixir, by going through these exercises were eye-opening for me, and I hope they were for you as well.
Understanding how to use processes by sending and receiving messages is paramount knowledge that you can use going forward on your Elixir journey.
Now, as promised - a small disappointment.
For more than 90% of the cases when you want to write concurrent code in
Elixir, you will not use processes like here. In fact, you won’t (almost)
ever use the send/2
and receive/1
functions. Seriously.
Why? Well, that’s because Elixir comes with this thingy called OTP, that will help you do much cooler things with concurrency, without writing any of this low-level process management code. Of course, this should not stop you from employing processes when you feel that you strongly need them, or when you want to experiment and learn.
If you want to read more about the basics of OTP, check out “Roll your own URL shortener using Elixir & GenServer”.
Some more reading #
If you would like to read a bit more, here are a couple of links that are worth checking out:
- Processes on Elixir’s “Getting started” guide
- Long-lived processes in Elixir by German Velasco on Thoughtbot’s blog
- Process on Hexdocs.pm
- Endless recursion on the Elixir Forum