Elixir Processes

Welcome to a tutorial on Processes in Elixir. Here you will learn about the basic constructs for spawning new processes, as well as sending and receiving messages between different processes.

 

All code runs inside processes in Elixir. however, processes are isolated from each other, but run concurrent to one another and communicate via message passing. Do not confuse Elixir’s processes with operating system processes. In Elixir, processes are extremely lightweight in terms of memory and CPU unlike threads in many other programming languages, as a result, it is not uncommon to have tens or even hundreds of thousands of processes running simultaneously.

 

The Spawn Function

The simplest and easiest way to create a new process is to use the spawn function. The spawn accepts a function that will run in the new process, as shown below. 

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

The output is:

false

 

As you can see, the return value of the spawn function is a PID. Hence, PID is a unique identifier for the process, so if you run the code above your PID, it will be different. From the above example, the process is dead when we check to see if it is alive because the process will exit as soon as it has finished running the given function.

As you already know, all Elixir codes run inside processes. Suppose you run the self function you will see the PID for your current session. This is shown below:

pid = self
 
Process.alive?(pid)

The output is:

true

 

Message Passing

Messages can be sent to a process with send and receive with receive.

Now, let’s pass a message to the current process and receive it on the same.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

The output is:

Hi people

From the above example, we sent a message to the current process using the send function and passed it to the PID of self, and then handled the incoming message by using the receive function.

Note that when a message is sent to a process, the message is stored in the process mailbox. Therefore, the receive block goes through the current process mailbox searching for a message that matches any of the given patterns. The receive block always supports guards and many clauses, such as case.

In the scenario where there is no message in the mailbox matching any of the patterns, the current process will wait until a matching message arrives. But, a timeout can be specified. Check out the example below.

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

The output is: 

nothing after 1s

Note that a timeout of 0 can be given when you already expect the message to be in the mailbox.

 

Links

In Elixir, the most common form of spawning is via the spawn_link function. Let’s check an example with spawn_link before we continue.

spawn fn -> raise "oops" end

The output is: 

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

You can see that it logged an error, but the spawning process is still running. It is running because the processes are isolated. But, if we want the failure in one process to propagate to another one, then we have to link them. The spawn_link function can be used to do this. Check out the example below. 

spawn_link fn -> raise "oops" end

The output is: 

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

When you run this in iex shell, the shell handles this error and does not exit. However, if you run by first making a script file and then using elixir <file-name>.exs, then the parent process will also be brought down due to this failure.

In Elixir, processes and links play an important role when building fault-tolerant systems. Also, in Elixir applications, we often link our processes to supervisors which will detect when a process dies and start a new process in its place. Interestingly, this is only possible because processes are isolated and don't share anything by default, and since processes are isolated, there is no way a failure in a process will crash or corrupt the state of the other process. 

As other languages will require us to catch/handle exceptions; in Elixir, we are fine with letting processes fail because we expect supervisors to properly restart our systems.

 

State

When building an application that requires state, for instance, to keep your application configuration, or you need to parse a file and keep it in memory, where would it be stored? Elixir's process functionality comes in handy when faced with such an issue.

Also, we can write processes that loop infinitely, maintain state, and send and receive messages. Check out an example where we will write a module that starts new processes that work as a key-value store in a file named kv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Take note that the start_link function starts a new process that runs the loop function, starting with an empty map. The loop function then waits for messages, and thereafter performs the appropriate action for each message. For the case of a :get a message, it sends a message back to the caller and calls the loop again, to wait for a new message. But, the :put message just invokes a loop with a new version of the map, with the given key and value stored. Now, run the code below: 

iex kv.exs

At this point, you should be in your iex shell. 

To test out our module, use the code below.

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

The output is: 

"Hello"