Atomic Counters in Elixir

In Elixir, we can use the :atomics module to work with atomic counters. Here’s how we can implement atomic counters:

defmodule AtomicCounters do
  def main do
    # We'll use an atomic reference to represent our (always-positive) counter.
    {:ok, counter} = :atomics.new(1, signed: false)

    # We'll start 50 tasks that each increment the counter exactly 1000 times.
    tasks = for _ <- 1..50 do
      Task.async(fn ->
        for _ <- 1..1000 do
          # To atomically increment the counter we use :atomics.add/3
          :atomics.add(counter, 1, 1)
        end
      end)
    end

    # Wait until all the tasks are done.
    Task.await_many(tasks)

    # Here no tasks are writing to the counter, but using :atomics.get/2
    # it's safe to atomically read a value even while other tasks are
    # (atomically) updating it.
    IO.puts("ops: #{:atomics.get(counter, 1)}")
  end
end

AtomicCounters.main()

In this Elixir implementation:

  1. We use :atomics.new/2 to create a new atomic counter. The first argument 1 specifies that we want a single counter, and signed: false indicates that it’s an unsigned integer.

  2. Instead of goroutines, we use Elixir’s Task module to create concurrent tasks.

  3. We use :atomics.add/3 to atomically increment the counter. The first argument is the atomic reference, the second is the index (1 in this case as we only have one counter), and the third is the value to add.

  4. We use Task.await_many/1 to wait for all tasks to complete, which is similar to WaitGroup.Wait() in the original example.

  5. Finally, we use :atomics.get/2 to safely read the final value of the counter.

To run this program:

$ elixir atomic_counters.exs
ops: 50000

We expect to get exactly 50,000 operations. If we had used a non-atomic integer and incremented it with regular addition, we’d likely get a different number, changing between runs, because the tasks would interfere with each other.

Elixir’s :atomics module provides thread-safe operations on individual numbers, making it suitable for scenarios where you need to share counters or flags between multiple processes without explicit locking.