Stateful Goroutines in Erlang

Our example demonstrates how to manage state using processes in Erlang. This approach aligns with Erlang’s philosophy of sharing data by message passing and having each piece of data owned by exactly one process.

-module(stateful_process).
-export([start/0]).

-record(read_op, {key, from}).
-record(write_op, {key, value, from}).

start() ->
    ReadOps = atomics:new(1, []),
    WriteOps = atomics:new(1, []),

    State = spawn(fun() -> loop(#{}) end),
    spawn(fun() -> reader(State, ReadOps) end),
    spawn(fun() -> writer(State, WriteOps) end),

    timer:sleep(1000),

    io:format("readOps: ~p~n", [atomics:get(ReadOps, 1)]),
    io:format("writeOps: ~p~n", [atomics:get(WriteOps, 1)]).

loop(StateMap) ->
    receive
        #read_op{key = Key, from = From} ->
            From ! {ok, maps:get(Key, StateMap, undefined)},
            loop(StateMap);
        #write_op{key = Key, value = Value, from = From} ->
            From ! ok,
            loop(maps:put(Key, Value, StateMap))
    end.

reader(State, Counter) ->
    State ! #read_op{key = rand:uniform(5), from = self()},
    receive
        {ok, _} -> ok
    end,
    atomics:add(Counter, 1, 1),
    timer:sleep(1),
    reader(State, Counter).

writer(State, Counter) ->
    State ! #write_op{key = rand:uniform(5), value = rand:uniform(100), from = self()},
    receive
        ok -> ok
    end,
    atomics:add(Counter, 1, 1),
    timer:sleep(1),
    writer(State, Counter).

In this example, our state is owned by a single process. This guarantees that the data is never corrupted with concurrent access. To read or write that state, other processes send messages to the owning process and receive corresponding replies.

We define read_op and write_op records to encapsulate the requests and provide a way for the owning process to respond.

The start/0 function initializes atomic counters for read and write operations, spawns the state-owning process and reader/writer processes, waits for a second, and then reports the operation counts.

The loop/1 function represents the state-owning process. It repeatedly receives messages, either responding to read requests or updating the state for write requests.

The reader/2 and writer/2 functions simulate multiple concurrent read and write operations. They send requests to the state-owning process, wait for responses, update the operation counters, and then repeat.

Running our program might show output similar to this:

$ erl -noshell -s stateful_process start -s init stop
readOps: 71708
writeOps: 7177

This Erlang-based state management example completes about 80,000 total operations in one second.

For this particular case, the process-based approach in Erlang is quite natural and idiomatic. It leverages Erlang’s built-in concurrency model and message passing, which aligns well with the language’s design principles. This approach can be particularly useful when dealing with multiple concurrent operations or when you need to maintain complex state. Always use the approach that feels most natural and leads to the clearest understanding of your program’s correctness.