Concurrency Basics
Processes
Processes in the Erlang VM are lightweight and run across all CPUs. While they may seem like native threads, they’re simpler and it’s not uncommon to have thousands of concurrent processes in an Elixir application.
The easiest way to create a new process is spawn
, which takes either an anonymous or named function. When we create a new process it returns a Process Identifier, or PID, to uniquely identify it within our application.
To start we’ll create a module and define a function we’d like to run:
defmodule Example do
def add(a, b) do
IO.puts(a + b)
end
end
Example.add(2, 3)
To evaluate the function asynchronously we use spawn/3:
spawn(Example, :add, [2, 3])
Message Passing
To communicate, processes rely on message passing. There are two main components to this: send/2
and receive
. The send/2
function allows us to send messages to PIDs. To listen we use receive
to match messages. If no match is found the execution continues uninterrupted.
defmodule ExampleB do
def listen do
receive do
{:ok, "hello"} -> IO.puts("World")
end
listen()
end
end
pid = spawn(ExampleB, :listen, [])
send(pid, {:ok, "hello"})
send(pid, :ok)
You may notice that the listen/0
function is recursive, this allows our process to handle multiple messages. Without recursion our process would exit after handling the first message.
Process Linking
One problem with spawn
is knowing when a process crashes. For that we need to link our processes using spawn_link
. Two linked processes will receive exit notifications from one another:
defmodule ExampleC do
def explode do
exit(:kaboom)
end
end
spawn(ExampleC, :explode, [])
# spawn_link(ExampleC, :explode, [])
Sometimes we don’t want our linked process to crash the current one. For that we need to trap the exits using Process.flag/2
. It uses erlang’s process_flag/2
function for the trap_exit
flag. When trapping exits (trap_exit
is set to true
), exit signals will be received as a tuple message: {:EXIT, from_pid, reason}
.
defmodule ExampleD do
def explode, do: exit(:kaboom)
def run do
Process.flag(:trap_exit, true)
spawn_link(ExampleC, :explode, [])
receive do
{:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
end
end
end
ExampleD.run()
Process Monitoring
What if we don’t want to link two processes but still be kept informed? For that we can use process monitoring with spawn_monitor
. When we monitor a process we get a message if the process crashes without our current process crashing or needing to explicitly trap exits.
defmodule ExampleE do
def explode, do: exit(:kaboom)
def run do
spawn_monitor(ExampleC, :explode, [])
receive do
{:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
end
end
end
ExampleE.run()
Agents
Agents are an abstraction around background processes maintaining state. We can access them from other processes within our application and node. The state of our Agent is set to our function’s return value:
{:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
Agent.update(agent, fn state -> state ++ [4, 5] end)
Agent.get(agent, & &1)
When we name an Agent we can refer to it by that instead of its PID:
Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
Agent.get(Numbers, & &1)
Tasks
Tasks provide a way to execute a function in the background and retrieve its return value later. They can be particularly useful when handling expensive operations without blocking the application execution
defmodule ExampleF do
def double(x) do
:timer.sleep(2000)
x * 2
end
end
task = Task.async(ExampleF, :double, [2000])
# Do some work
Task.await(task)