Understanding the basics of Elixir’s concurrency model

pidfalseThis 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 endendIf 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 endendSo 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.

":okWhat 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.

In 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 the Store processput – adds a new key-value pair to the state mapget – fetches a value for a given key from the state mapget_all – fetches all of the key-value pairs that are stored in the state mapPutting a valueLet’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 endendWhen 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 valuesLet’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 endendJust 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"}:okiex(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"}:okIf 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 storeRight 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 endendIf 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"}:okAs 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 disappointmentFirst, 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.

But, that’s a topic for the next blog post, where we’ll dive more in OTP and some of its behaviours.

Until then, where do you see processes as a potential use case in your projects/work?Some more readingIf you would like to read a bit more, here are couple of links that are worth checking out:Processes on Elixir’s “Getting started” guideLong-lived processes in Elixir by German Velasco on Thoughtbot’s blogProcess on Hexdocs.

pmEndless recursion on the Elixir Forum.

. More details

Leave a Reply