Understanding Processes for Elixir Developers
- Carlo Gilmar
- 21st Apr 2022
- 14 min of reading time
This post is for all developers who want to try Elixir or are trying their first steps in Elixir. This content is aimed at those who already have previous experience with the language.
This will help to explain one of the most important concepts in the BEAM: processes. Although Why understand processes?
Scalability, fault-tolerance, concurrent design, distributed systems – the list of things that Elixir famously make easy is long and continues to grow. All these features come from the Erlang Virtual Machine, the BEAM. Elixir is a general-purpose programming language. You can learn the basics of this functional programming language without understanding processes, especially if you come from another paradigm. But, understanding processes is an excellent way to grow your understanding of what makes Elixir so powerful and how to harness it for yourself because it represents one of the key concepts from the BEAM world. A process is an isolated entity where code execution happens. Processes are everywhere in an Erlang system, note the iex, the observer and the OTP patterns are examples of how a process looks. They are lightweight, allowing us to run concurrent programs, and build distributed and fault-tolerant designs. There are a wide variety of other processes using the Erlang observer application. Because of how the BEAM runs, you can run a program inside a module without ever knowing the execution is a process. Most Elixir users would have at least heard of OTP. OTP allows you to use all the capabilities of the BEAM, and understanding what happens behind the scenes will help you to take full advantage of abstractions when designing processes. A process is an isolated entity where code execution happens and they are the base to design software systems taking advantage of the BEAM features. You can understand what a process is as just a block of memory where you’re going to store data and manipulate it. A process is a built-in memory with the following parts: We could define the process lifecycle for now as: The function spawn helps us to create a new process. We will need to provide a function to be executed inside it, this will return the process identifier PID. Elixir has a module called Process to provide functions to inspect a process. Let’s look at the following example using the iex: A process is an entity that executes code inside but also can receive and process messages from other processes (they can communicate to each other only by messages). Sometimes this might be confusing, but these are different and complementary parts. The receive statement helps us to process the messages stored in the mailbox. You can send messages using the send statement, but the process will only store them in the mailbox. To start to process them, you should implement the receive statement, this will put the process into a mode that waits for sent messages to arrive. TERMINATING: Once the message has been processed, our process will die. One option is to enable the process to run and process messages from the mailbox. Remember how the receive/0 statement works: we could just call this statement after processing a message to make a continuous cycle and prevent termination. Up to this point we have understood how to create a new process, process messages from the mailbox and how to keep it alive. The mailbox is an important part of the process where you can store messages, but we have other parts of memory that allow us to keep an internal state. Let’s see how to hold an internal state. CODE EXECUTION: Use the Process.alive?/1 and verify that our process is alive. Well done! I hope all of these examples and explanations were enough to illustrate what a process is. It’s important to keep in mind the anatomy and the life cycle,to understand what’s happening behind the scenes. You can design with the process, or with OTP abstractions. But the concepts behind this are the same, let’s look at an example with Phoenix Live View: While the functions render/1 and mount/3 allow you to set up the Live View, the functions handle_info/2 and handle_event/3 are updating the socket, which is an internal state. Does this sound familiar to you? This is a process! A live view is an OTP abstraction to create a process behind the scenes, and of course this contains other implementations. For this particular case the essence of the process is present when the Live View reloads the HTML, keeps all the variables inside the state, and handles all the interactions while modifying it. Understanding processes gives you the concepts to understand how the BEAM works and to learn how to design better programs. Many of the libraries written in Elixir or the OTP abstractions these concepts as well, so next time you use one of these in your projects, think about these explanations to better understand what’s happening under the hood. Thanks for reading this. If you’d like to learn more about Elixir check out our training schedule or join us at ElixirConf EU 2022. Carlo Gilmar is a software developer at Erlang Solutions based in Mexico City. He started his journey as a developer at Making Devs, he’s the founder of Visual Partner-Ship, a creative studio to mix technology and visual thinking. Defining a process in the BEAM world
Process lifecycle
Process creation
iex(1)> execute_fun = fn -> IO.puts "Hi! ⭐️ I'm the process #{inspect(self())}." end
#Function<45.65746770/0 in :erl_eval.expr/5>
iex(2)> pid = spawn(execute_fun)
Hi! ⭐️ I'm the process #PID<0.234.0>.
#PID<0.234>
iex(2)> Process.alive?(pid)
false
Receiving messages
iex(1)> defmodule MyProcess do
...(1)> def awaiting_for_receive_messages do
...(1)> IO.puts "Process #{inspect(self())}, waiting to process a message!"
...(1)> receive do
...(1)> "Hi" ->
...(1)> IO.puts "Hi from me"
...(1)> "Bye" ->
...(1)> IO.puts "Bye, bye from me"
...(1)> _ ->
...(1)> IO.puts "Processing something"
...(1)> end
...(1)> IO.puts "Process #{inspect(self())}, message processed. Terminating..."
...(1)> end
...(1)> end
iex(2)> pid = spawn(MyProcess, :awaiting_for_receive_messages, [])
Process #PID<0.125.0>, waiting to process a message!
#PID<0.125.0>
iex(3)> Process.alive?(pid)
true
iex(4)> send(pid, "Hi")
Hi from me
"Hi"
Process #PID<0.125.0>, message processed. Terminating...
iex(5)> Process.alive?(pid)
false
Keeping the process alive
iex(1)> defmodule MyProcess do
...(1)> def awaiting_for_receive_messages do
...(1)> IO.puts "Process #{inspect(self())}, waiting to process a message!"
...(1)> receive do
...(1)> "Hi" ->
...(1)> IO.puts "Hi from me"
...(1)> awaiting_for_receive_messages()
...(1)> "Bye" ->
...(1)> IO.puts "Bye, bye from me"
...(1)> awaiting_for_receive_messages()
...(1)> _ ->
...(1)> IO.puts "Processing something"
...(1)> awaiting_for_receive_messages()
...(1)> end
...(1)> end
...(1)> end
iex(2)>
nil
iex(3)> pid = spawn(MyProcess, :awaiting_for_receive_messages, [])
Process #PID<0.127.0>, waiting to process a message!
#PID<0.127.0>
iex(4)> Process.alive?(pid)
true
iex(5)> send(pid, "Hi")
Hi from me
Process #PID<0.127.0>, waiting to process a message!
"Hi"
iex(6)> Process.alive?(pid)
true
Hold the state
defmodule MyProcess do
def awaiting_for_receive_messages(messages_received \\ []) do
receive do
"Hi" = msg ->
IO.puts "Hi from me"
[msg|messages_received]
|> IO.inspect(label: "MESSAGES RECEIVED: ")
|> awaiting_for_receive_messages()
"Bye" = msg ->
IO.puts "Bye, bye from me"
[msg|messages_received]
|> IO.inspect(label: "MESSAGES RECEIVED: ")
|> awaiting_for_receive_messages()
msg ->
IO.puts "Processing something"
[msg|messages_received]
|> IO.inspect(label: "MESSAGES RECEIVED: ")
|> awaiting_for_receive_messages()
end
end
end
iex(3)> pid = spawn(MyProcess, :awaiting_for_receive_messages, [])
#PID<0.132.0>
iex(4)> Process.alive?(pid)
true
iex(5)> send(pid, "Hi")
Hi from me
"Hi"
MESSAGES RECEIVED: : ["Hi"]
iex(6)> send(pid, "Bye")
Bye, bye from me
"Bye"
MESSAGES RECEIVED: : ["Bye", "Hi"]
iex(7)> send(pid, "Heeeey!")
Processing something
"Heeeey!"
MESSAGES RECEIVED: : ["Heeeey!", "Bye", "Hi"]
How to integrate Elixir reasoning in your processes
defmodule DemoWeb.ClockLive do
use DemoWeb, :live_view
def render(assigns) do
~H"""
<div>
<h2>It's <%= NimbleStrftime.format(@date, "%H:%M:%S") %></h2>
<%= live_render(@socket, DemoWeb.ImageLive, id: "image") %>
</div>
"""
end
def mount(_params, _session, socket) do
if connected?(socket), do: Process.send_after(self(), :tick, 1000)
{:ok, put_date(socket)}
end
def handle_info(:tick, socket) do
Process.send_after(self(), :tick, 1000)
{:noreply, put_date(socket)}
end
def handle_event("nav", _path, socket) do
{:noreply, socket}
end
defp put_date(socket) do
assign(socket, date: NaiveDateTime.local_now())
end
end
About the author
Join Lorena in this years Advent of Code 2024. She'll be solving daily puzzles throughout the month of December.
Attila Sragli explores the BEAM VM's inner workings, comparing them to the JVM to highlight their importance.
Pawel Chrząszcz introduces MongooseIM 6.3.0 with Prometheus monitoring and CockroachDB support for greater scalability and flexibility.