Do you want to pick up from where you left of?
Take me there

Concurrency

One of the selling points of Elixir is its support for concurrency. Thanks to the Erlang VM (BEAM), concurrency in Elixir is easier than expected. The concurrency model relies on Actors, a contained process that communicates with other processes through message passing.

In this lesson we’ll look at the concurrency modules that ship with Elixir.

In the following chapter we cover the OTP behaviors that implement them.

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

iex> Example.add(2, 3)
5
:ok

To evaluate the function asynchronously we use spawn/3:

iex> spawn(Example, :add, [2, 3])
5
#PID<0.80.0>

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 Example do
  def listen do
    receive do
      {:ok, "hello"} -> IO.puts("World")
    end

    listen()
  end
end

iex> pid = spawn(Example, :listen, [])
#PID<0.108.0>

iex> send pid, {:ok, "hello"}
World
{:ok, "hello"}

iex> send pid, :ok
: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 Example do
  def explode, do: exit(:kaboom)
end

iex> spawn(Example, :explode, [])
#PID<0.66.0>

iex> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.57.0>) evaluator process exited with reason: :kaboom

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 Example do
  def explode, do: exit(:kaboom)

  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])

    receive do
      {:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

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 Example do
  def explode, do: exit(:kaboom)

  def run do
    spawn_monitor(Example, :explode, [])

    receive do
      {:DOWN, _ref, :process, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

iex> Example.run
Exit reason: kaboom
:ok

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:

iex> {:ok, agent} = Agent.start_link(fn -> [1, 2, 3] end)
{:ok, #PID<0.65.0>}

iex> Agent.update(agent, fn (state) -> state ++ [4, 5] end)
:ok

iex> Agent.get(agent, &(&1))
[1, 2, 3, 4, 5]

When we name an Agent we can refer to it by that instead of its PID:

iex> Agent.start_link(fn -> [1, 2, 3] end, name: Numbers)
{:ok, #PID<0.74.0>}

iex> Agent.get(Numbers, &(&1))
[1, 2, 3]

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 Example do
  def double(x) do
    :timer.sleep(2000)
    x * 2
  end
end

iex> task = Task.async(Example, :double, [2000])
%Task{
  owner: #PID<0.105.0>,
  pid: #PID<0.114.0>,
  ref: #Reference<0.2418076177.4129030147.64217>
}

# Do some work

iex> Task.await(task)
4000
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!