Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Concurrency Basics

ConcurrencyBasics.livemd

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)