Processes
Lesson Overview
# Introduction
About
In Elixir, all code runs inside processes. Elixir processes:
- Should not be confused with system processes.
- Are lightweight.
- Have specific use cases. They can:
- Keep global state.
- Contain failure.
- Allow for concurrent and asynchronous code.
- It is normal to have an Elixir app that runs hundreds of processes, but also one that doesn’t explicitly create new processes at all, especially if it’s a library.
Creating processes
By default, a function will execute in the same process from which it was called. When you need to explicitly run a certain function in a new process, use spawn:
-
spawn/1accepts a function that it will execute directly.spawn(fn -> 2 + 2 end) # => #PID<0.125.0> -
spawn/3accepts a function that it will execute by the module name, the function name (as atom), and a list of arguments to pass to that function.spawn(String, :split, ["hello there", " "]) # => #PID<0.113.0>- This data triplet is often called an MFA — Module, Function, Arguments.
-
A process exits as soon as its function has finished executing.
-
You can check if a process is still alive (executing) with
Process.alive?/1:pid = spawn(fn -> 2 + 2 end) Process.alive?(pid) # => false
Messages
Processes do not directly share information with one another. Processes send messages to share data. This concurrency pattern is called the Actor model.
-
Send messages to a process using
send/2.send(pid, :hello)- The message ends up in the recipient’s mailbox in the order that they are sent.
senddoes not check if the message was received nor if the recipient is still alive.
-
A message can be of any type.
-
You can receive a message sent to the current process using
receive/1.- You need to pattern match on messages.
receivewaits until one message matching any given pattern is in the process’s mailbox.- By default, it waits indefinitely, but can be given a timeout using an
afterblock.
- By default, it waits indefinitely, but can be given a timeout using an
- Read messages are removed from the process’s mailbox. Unread messages will stay there indefinitely.
- Always write a catch-all
_clause inreceive/1to avoid running of out memory due to piled up unread messages.
- Always write a catch-all
receive do {:ping, sender_pid} -> send(sender_pid, :pong) _ -> nil after 5000 -> {:error, "No message in 5 seconds"} end
Receive loop
If you want to receive more than one message, you need to call receive/1 recursively. It is a common pattern to implement a recursive function, for example named loop, that calls receive/1, does something with the message, and then calls itself to wait for more messages. If you need to carry some state from one receive/1 call to another, you can do it by passing an argument to that loop function.
def loop(state) do
receive do
:increment_by_one ->
loop(state + 1)
{:report_state, sender_pid} ->
send(sender_pid, state)
loop(state)
:stop ->
nil
_ ->
loop(state)
end
end
In practice, this approach is rarely used directly. Elixir offers concurrency abstractions, such as the Agent module or a GenServer behaviour, that both build on top of the receive loop. However, it is crucial to understand those basics to be able to efficiently use the abstractions.
Originally from Exercism elixir concepts