Mutexes in Elixir

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple processes.

defmodule Container do
  use GenServer

  def start_link(initial_state) do
    GenServer.start_link(__MODULE__, initial_state)
  end

  def init(counters) do
    {:ok, %{counters: counters}}
  end

  def handle_call({:inc, name}, _from, state) do
    updated_counters = Map.update(state.counters, name, 1, &(&1 + 1))
    {:reply, :ok, %{state | counters: updated_counters}}
  end

  def handle_call(:get_counters, _from, state) do
    {:reply, state.counters, state}
  end
end

defmodule Main do
  def run do
    {:ok, container} = Container.start_link(%{"a" => 0, "b" => 0})

    do_increment = fn name, n ->
      Enum.each(1..n, fn _ ->
        GenServer.call(container, {:inc, name})
      end)
    end

    tasks = [
      Task.async(fn -> do_increment.("a", 10000) end),
      Task.async(fn -> do_increment.("a", 10000) end),
      Task.async(fn -> do_increment.("b", 10000) end)
    ]

    Enum.each(tasks, &Task.await/1)

    counters = GenServer.call(container, :get_counters)
    IO.inspect(counters)
  end
end

Main.run()

In this Elixir version, we use a GenServer to manage shared state, which is analogous to using a mutex in Go. The Container module implements the GenServer behavior, providing a way to safely access and modify the counters from multiple processes.

The Container module defines two main operations:

  • inc/2: Increments a named counter
  • get_counters/0: Retrieves the current state of all counters

In the Main module, we create a Container process and spawn three tasks that concurrently increment the counters. We use Task.async/1 to create these concurrent tasks, which is similar to using goroutines in Go.

After all tasks complete, we retrieve and print the final state of the counters.

Running the program shows that the counters are updated as expected:

$ elixir mutexes.exs
%{"a" => 20000, "b" => 10000}

This example demonstrates how to use Elixir’s built-in concurrency primitives to safely manage shared state across multiple processes. While Elixir encourages a more functional and message-passing oriented approach to concurrency, this example shows how you can achieve mutex-like behavior when needed.

Next, we’ll look at implementing this same state management task using only processes and message passing, which is more idiomatic in Elixir.