cult3

Working with Processes in Elixir

Jul 11, 2016

Table of contents:

  1. What are Processes?
  2. Spawning a new Process
  3. Sending and receiving messages between Processes
  4. Linking Processes together
  5. Conclusion

Over the last couple of weeks we’ve explored many of the foundational elements of the Elixir programming language. If you are coming to Elixir from a programming language such as PHP, Ruby, or Python, you have probably already learned a lot. Elixir is a functional programming language and so it takes a new way of thinking when you are coming from an object-oriented background.

However, I think we have reached the point where we need to move on from the basics. It’s time to explore the next big section of what makes Elixir (and Erlang) a very interesting programming language.

For the next couple of weeks we are going to be looking at processes, concurrency, and how Elixir deals with fault tolerance. Building on the foundation of Erlang and OTP, we can build applications and services with incredibly high availability.

But with that being said, we aren’t going to worry too much about the bigger picture right now. In today’s tutorial we will be looking at working with processes in Elixir.

What are Processes?

Although we have never mentioned processes up until this point in our exploration of Elixir, whenever we have been running code it has been running in a process. All Elixir code, even when you use iex is running in a process.

Processes are lightweight, isolated, and run concurrently with each other. You can communicate between processes via message passing.

Processes are also very small in terms of memory and CPU, and so you could have hundreds of thousands of processes running simultaneously on a single machine. This is in stark contrast to operating system processes or threads in other programming languages that consume a lot of resources.

A good analogy I’ve heard to describe Elixir processes are that they are like objects in an object-oriented programming language. In an OOP language, an object encapsulates data and behavior, and the only way to communicate with the object is via message passing.

Thinking of Elixir processes as being equivalent to objects in an OOP language is useful because it means you don’t get caught up in the fear of using operating system processes or threads. It’s very easy to create a new object in an OOP language, and the same can be said for a process in Elixir.

But unlike objects in an OOP language, processes are isolated from each other and run concurrently. This has a number of huge implications and it is the foundation of how we can build distributed and fault-tolerant applications in Elixir and Erlang.

Spawning a new Process

So hopefully you now have a good understanding of what a process is in Elixir and how they are typically used in an Elixir application, let’s now take a look at spawning a new process.

The easiest way to create a new process is to use the spawn/1 function. The spawn/1 accepts a function that will be run in the new process:

spawn(fn -> 2 * 2 end)
# PID<0.65.0>

The return value of the spawn/1 function is a PID. This is a unique identifier for the process and so if you run the code above your PID will be different from mine.

You can check to see if a process is still alive by passing the PID to the Process.alive?/1 function:

pid = spawn(fn -> 2 * 2 end)
# PID<0.67.0>

Process.alive?(pid)
# false

As you can see in this example the process is dead when we check to see if it alive. This is because the process will exit as soon as it has finished running the given function.

Earlier in this article I mentioned that all Elixir code runs inside of a process, including iex. If you run the self/0 function you will see the PID for your current session:

self
# PID<0.57.0>

You can also check to see if the process is alive:

pid = self
# PID<0.57.0>

Process.alive?(pid)
# true

So as you can see, the current iex session process is still alive (which makes sense because we are still using it!).

Sending and receiving messages between Processes

One of the most important aspects of processes that I mentioned earlier was the fact that they communicate via messages. Each process has a mailbox that can accept messages from other processes. It is then up to the process who received the message to take an action or ignore it.

To send a message, you can use the send/2 function. This function accepts a PID and a message to be sent:

send(self, :hello)
# :hello

In this example I’m sending a message of :hello to the current process.

Now that we’ve sent this message to the current process the message will be sat in the process mailbox. We can see this by running the flush/0 function. This will list the messages in the mailbox and then empty it:

flush
# :hello
# :ok

The message can be anything you want:

send(self, {:greeting, "all of your base are belong to us"})

In this example I’m sending a tuple (Using Tuples in Elixir).

You can act on the messages by using the receive construct. The receive block will go through all of the messages in the mailbox looking for a match:

receive do
  {:greeting, msg} -> msg
end

The receive block is using pattern matching and can also use guard clauses like we saw in Understanding Pattern Matching in Elixir, Multi-clause Functions with Pattern Matching and Guards in Elixir, and Branching and Conditionals in Elixir.

If no message matches any of the clauses the process will just sit and wait for a new message to come in. Alternatively you can set a timeout that will end waiting for a message to match:

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

Linking Processes together

Another very important characteristic of Elixir processes is that they are totally isolated from one another. This means if you spawn a process and something goes wrong, the original process will continue to run.

For example, if you spawn a process that raises an exception, you will see that the original process does not crash:

spawn(fn -> raise "smell ya later" end)
# PID<0.69.0>

# [error] Process #PID<0.69.0> raised an exception

Here we can see that the exception is logged, but the original process is still running without disruption.

However sometimes you do want the fact that a process has crashed to propagate to another process. This can be achieved by using the spawn_link/1 function instead:

spawn_link(fn -> raise "smell ya later" end)
# ** (EXIT from #PID<0.57.0>) an exception was raised:
# ** (RuntimeError) smell ya later
# :erlang.apply/2

# 12:07:01.780 [error] Process #PID<0.72.0> raised an exception
# ** (RuntimeError) smell ya later
# :erlang# .apply/2

As you can see from the error output, the exception propagated up to the original process because we used the spawn_link/1 function instead.

Conclusion

Using Elixir and Erlang it is possible to write very highly fault-tolerant applications. The basis of this is that everything in Elixir runs inside of a process.

Processes are very lightweight and are more analogous to objects in object-oriented programming than they are to OS processes or threads in other programming languages. Processes are isolated from each other and so if one process crashes it won’t touch any other process unless you specifically want to be notified of the crash.

You can pass messages between processes just like you can pass messages between objects in object-oriented programming languages.

In next week’s tutorial we will continue to look at processes and how they enable concurrency and parallelism.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.