Atomic Counters in Erlang

Our primary mechanism for managing state in Erlang is through message passing between processes. However, there are other options for managing state. Here we’ll look at using Erlang’s built-in support for atomic operations to create atomic counters accessed by multiple processes.

-module(atomic_counters).
-export([main/0]).

main() ->
    % We'll use an atomic integer to represent our (always-positive) counter.
    Ops = atomics:new(1, [{signed, false}]),

    % We'll start 50 processes that each increment the counter exactly 1000 times.
    Pids = [spawn(fun() -> increment_counter(Ops, 1000) end) || _ <- lists:seq(1, 50)],

    % Wait until all the processes are done.
    [receive {Pid, done} -> ok end || Pid <- Pids],

    % Here no processes are writing to 'Ops', but it's safe to atomically read 
    % a value even while other processes are (atomically) updating it.
    io:format("ops: ~p~n", [atomics:get(Ops, 1)]).

increment_counter(Ops, 0) ->
    self() ! {self(), done};
increment_counter(Ops, Count) ->
    % To atomically increment the counter we use add/3.
    atomics:add(Ops, 1, 1),
    increment_counter(Ops, Count - 1).

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

To run the program:

$ erlc atomic_counters.erl
$ erl -noshell -s atomic_counters main -s init stop
ops: 50000

In Erlang, we use processes instead of goroutines, and we use the atomics module for atomic operations. The atomics:new/2 function creates a new atomic counter, atomics:add/3 increments it, and atomics:get/2 reads its value.

Instead of using a WaitGroup, we spawn processes and wait for them to send a message when they’re done. This achieves the same synchronization as the Go example.

Next, we’ll look at other concurrency primitives in Erlang for managing state.