In Elixir, a process is a lightweight concurrent computation unit that runs independently of other processes. These processes are not operating system processes but are virtual processes managed by the Erlang Virtual Machine (BEAM).
Elixir Processes are:
Lightweight: Elixir processes are lightweight in terms of memory and CPU usage. It is common to have tens to hundreds of thousands of processes running concurrently in an Elixir application.
Isolated: Each Elixir process has its own memory space and does not share memory with other processes. This isolation ensures that processes do not interfere with each other's data.
Message Passing: Processes communicate with each other by sending and receiving messages. Messages are stored in the process mailbox and are processed in the order they are received. This message-passing mechanism enables coordination and synchronization between processes.
Fault-Tolerant: Elixir processes are designed to be fault-tolerant. If a process crashes due to an error, it does not affect the rest of the system. Supervision strategies can be used to monitor and restart processes in case of failures.
Concurrency: Elixir processes enable concurrent execution of tasks, allowing different parts of the application to run simultaneously. This concurrency model is based on the actor model, where each process behaves like an independent actor processing messages.
Scalability: By leveraging processes for concurrency, Elixir applications can scale across multiple CPU cores without the need for explicit management of threads or locks. The BEAM scheduler efficiently distributes processes across available CPU resources.
Spawning A New Process
In Elixir, you can spawn a new process using the spawn/1
function or the Task
module. Here's how you can spawn a process using the first method:
The spawn/1
function takes a function as an argument and executes that function in a new process. Here's an example:
self()
# Output: PID<0.109.0>
# Spawning a process using spawn/1
pid = spawn(fn ->
IO.puts("Hello from a new process!")
end)
IO.puts("Spawned process PID: #{inspect pid}")
# Output: Spawned process PID: #PID<0.148.0>
In the above example, a new process is created to execute the specified function concurrently with the main process. The spawned process runs independently and can perform its tasks concurrently with other processes in the system.
When you spawn a process, you receive a Process Identifier (PID) that uniquely identifies the new process. This PID can be used to send messages to the process or monitor its status.
Spawning processes in Elixir allow you to achieve concurrency and parallelism in your applications, enabling tasks to run concurrently and take advantage of multi-core processors efficiently.
Message Passing Between Processes
Let's take this one step further to illustrate message passing between two processes. First, we will spawn a process that can receive messages, and then send a message to that process. Finally, we will check the process status:
# Spawning a process to handle messages
pid = spawn(fn ->
receive do
:add -> IO.puts("Received :add message")
{:multiply, a, b} -> IO.puts("Received :multiply message with values #{a} and #{b}")
end
end)
# Output: #PID<0.150.0>
# Checking if the process is alive
IO.puts("Is process alive? #{inspect Process.alive?(pid)}")
# Output: Is process alive? true
# Sending message to the process
send(pid, :add) # Output: Received :add message
# Checking if the process is still alive after processing messages
IO.puts("Is process alive? #{inspect Process.alive?(pid)}") # Output: Is process alive? false
In the above example:
We spawn a new process that waits to receive messages.
The process pattern matches on the messages it receives (
:add
and{:multiply, a, b}
).We check if the process is alive before sending messages to it.
A message is sent to the process using the
send/2
function.The process handles the messages and prints corresponding output based on the message received.
We check if the process is alive after sending messages to it.
You can run this code in an Elixir environment (such as IEx) to observe the process of handling messages and their status before and after message processing.
So, now we are going to take it one step further yet again and ensure the newly spawned process lasts forever using an infinite loop. Don't worry. We will make use of recursion to create an infinite loop, but it will be controlled and, thus, manageable as you will see in the following example:
defmodule MyExample.Spawn.ForEver do
def start do
spawn(&loop/0)
end
defp loop do
receive do
{:greet, name} ->
IO.puts("Hello, #{name}!")
loop()
{:calculate, operation, a, b} ->
result =
case operation do
:add -> a + b
:subtract -> a - b
:multiply -> a * b
:divide -> a / b
end
IO.puts("Result of #{operation}: #{result}")
loop()
:status ->
{:memory, bytes} = Process.info(self(), :memory)
IO.puts("Process is using #{bytes} bytes")
loop()
:crash ->
raise "Let it crash!"
:bye ->
IO.puts("Goodbye, my friend!")
end
end
end
# Start the custom process
pid = MyExample.Spawn.ForEver.start() # Output: #PID<0.157.0>
# Send messages to the process
send(pid, {:greet, "Sara"}) # Output: Hello, Sara!
send(pid, {:calculate, :add, 10, 5}) # Output: Result of add: 15
send(pid, :status) # Output: Process is using 2632 bytes
send(pid, :bye) # Output: Goodbye, my friend!
# OR, alternatively
send(pid, :crash) # Output: [error] Process #PID<0.157.0> raised an exception ** (RuntimeError) Let it crash!
In the above example:
We define a module
MyExample.Spawn.ForEver
with astart
function to spawn a process that runs the privateloop/0
function.The private
loop/0
function uses pattern matching in thereceive
block to handle different types of messages:{:greet, name}
: Greets a person by name.{:calculate, operation, a, b}
: Performs arithmetic operations based on the specified operation.:status
: Retrieves memory usage information of the process.:crash
: Raises an exception to simulate a crash.:bye
: Prints a farewell message.
Messages are sent to the process to trigger different behaviors, such as greetings, calculations, status checks, crashes, and farewells.
You can run this code in an Elixir environment to observe how the custom process handles various messages and behaviors.
Isolating A Crashed Process
And finally, let's spawn two processes and let one of them crash. What do you think will happen? Will the crashed process take down the other process with it? You can probably guess the answer, but it is still good to see it for yourself.
Here is a new code example that demonstrates spawning multiple processes and simulating a crash in one of them:
defmodule Examples.Spawn.MultipleProcesses do
def start do
spawn(&loop/0)
end
defp loop do
receive do
:crash ->
raise "You just made me crash!"
end
end
end
# Start two processes
p1 = Examples.Spawn.MultipleProcesses.start()
p2 = Examples.Spawn.MultipleProcesses.start()
# Simulate a crash in process p1
send(p1, :crash)
# Check the status of both processes
IO.inspect([Process.alive?(p1), Process.alive?(p2)])
# Output: [false, true]
In this code example:
We define a module
Examples.Spawn.MultipleProcesses
with astart
function to spawn processes that run theloop/0
function.The
loop/0
function waits to receive a:crash
message and raises an exception to simulate a crash.Two processes (
p1
andp2
) are started using thestart
function.We send a
:crash
message to processp1
to simulate a crash in that process.Finally, we check the status of both processes using
Process.alive?
to see if the processp1
is no longer alive after the crash.
In this straightforward demonstration, we observe that error isolation is effortlessly achieved without any explicit actions on our part. Notably, attempting to crash one process from another is not feasible within the Elixir environment.
Overall, Elixir processes play a key role in achieving concurrency, scalability, fault tolerance, and isolation in Elixir applications. They form the foundation of Elixir's concurrency model and are essential for building responsive and resilient systems.