Processes
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
Processes
> In Elixir, all code runs inside processes. > Processes are isolated from each other, run concurrent to one another > and communicate via message passing. Processes are not only the basis for concurrency in Elixir, > but they also provide the means for building distributed and fault-tolerant programs. > > * Elixir Documentation
So all Elixir code runs inside of a process.
flowchart
subgraph Process
E[Elixir Code]
end
Processes are isolated from each other and communicate via message passing.
sequenceDiagram
Process1 ->> Process2: message
Processes can store state and allow us to have in-memory persistence.
flowchart
subgraph Process
State
end
You’ve been using processes for some time now. Each Elixir cell in livebook is a process.
It even has a pid (personal identifier) that we can see with self()
.
self()
We can send/2
and receive/1
messages between processes by using their pid. Likewise, a process can send its self()
a message.
send(self(), "message")
receive do
"message" -> "received"
end
The left-hand side of the receive expression can pattern match on any value.
It’s much like a case
statement.
send(self(), {:hello, "world"})
receive do
{:hello, payload} -> payload
end
We can spawn/1
a new process and get its pid.
sequenceDiagram
Parent Process->>Child Process: spawns
activate Child Process
Child Process-->>Parent Process: pid
deactivate Child Process
spawn(fn -> nil end)
A process accepts a callback function and ends when that callback function completes. A process is alive and then it dies.
flowchart LR
spawn --> Process --> Alive --> cn[Callback Function] --> Dead
We can use the Process module for functionality
related to processes. We’ll use the alive?/1
function to show that the process is alive before executing its callback function.
pid = spawn(fn -> IO.puts("I was called") end)
Process.alive?(pid) && IO.puts("I am alive!")
We can use Process.sleep/1
to pause the execution and show that the spawned process dies
after it is called.
pid = spawn(fn -> IO.puts("I was called") end)
Process.sleep(100)
Process.alive?(pid) || IO.puts("I am dead :(")
Processes are isolated from each other. That means that when a child process raises an error, it will not crash the parent process. Instead, it will only log an error.
sequenceDiagram
Parent Process->>Child Process: spawns
activate Child Process
Child Process-->>Parent Process: pid
Child Process->>Child Process: raise
activate Child Process
Child Process->>Parent Process: logs termination
deactivate Child Process
Child Process->>Child Process: dies
deactivate Child Process
spawn(fn -> raise "oops" end)
"the above crashes, but I will keep running."
If this is not desirable behavior, we can link processes together so that if one dies, the other will crash.
flowchart LR
subgraph Child
CP[Child Process] --> raise --dies--> CP
end
subgraph Parent
PA[Parent Process]
end
PA --spawns_link--> Link --> CP
raise --crash--> PA
We can either spawn_link/1
.
pid1 = spawn_link(fn -> raise "oops" end)
"I will not run, because the above crashes"
Or manually link a process with Process.link/1
.
pid1 = spawn(fn -> raise "oops" end)
Process.link(pid)
"I will not run, because the above crashes"
Your Turn
Spawn a linked process and crash this Elixir cell below. Comment out your solution when complete to avoid continuously crashing the livebook!
Message Passing
By spawning two processes, they can communicate back and forth with send/2
and receive/1
.
Let’s spawn a process in one cell and send a message in another.
flowchart LR
subgraph P1[Process 1]
pid -->
receive
end
subgraph Process 2
P2 --> send --> pid
end
pid1 =
spawn(fn ->
receive do
"message" -> IO.puts("received!")
end
end)
Evaluate the cell above to create a process waiting to receive a message, then evaluate
the cell below to send that message a process. You’ll notice the IO.puts/1
logs in the cell below.
As soon as the spawned process receives a message, it dies. You’ll notice you can only send and receive a single message. You can re-evaluate the cell above and the cell below to repeat the example.
send(pid1, "message")
Your Turn
In the Elixir cell below, spawn a new process and send it a message {:hello, "world"}
.
IO.puts
the message’s payload where "world"
is the payload.
State
So far, we spawn a process that receives a single message and then dies.
flowchart LR
P1[Process] --send-->
P2[Process2] --> receive --dies--> P2
We can also create a process that can receive many messages by leveraging a recursive function.
This recursive function will continue to call receive indefinitely, so the process should keep receiving messages and stay alive.
flowchart LR
Process --> loop --> receive --> loop
defmodule ServerProcess do
def loop do
IO.puts("called #{Enum.random(1..10)}")
receive do
"message" -> loop()
end
end
end
server_process = spawn(fn -> ServerProcess.loop() end)
We’ve used Enum.random/1
to show that the process continues to loop and receive messages.
send(server_process, "message")
With a slight modification of the ServerProcess
, we can store state!
flowchart LR
ServerProcess --initial state--> loop --state--> receive --new state--> loop
We’ll store the state as an integer to create a counter.
flowchart LR
CounterProcess --> loop --0--> receive --1--> loop
defmodule CounterProcess do
def loop(state \\ 0) do
IO.inspect(state, label: "counter")
receive do
"increment" -> loop(state + 1)
end
end
end
counter_process = spawn(fn -> CounterProcess.loop() end)
Try evaluating the cell below over and over again. Notice that the counter value increments! We now have stateful processes!
send(counter_process, "increment")
Stateful processes are short-term in-memory persistence. We can create a stateful process to store some value for the duration of the program.
Your Turn
Create a TodoListProcess
which can receive messages in a loop.
todo_list_process = spawn(fn -> TodoListProcess.loop() end)
It should store a list
of strings in the state. You should be able to send messages to the TodoListProcess
to
add
and remove
items from the list in the state.
send(todo_list_process, {:add, "new todo list item"})
send(todo_list_process, {:remove, "new todo list item"})
defmodule TodoListProcess do
def loop(state \\ []) do
end
end
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish processes section"