Following the advice of Dave Thomas who said: “any developer should learn a new language every year” on RailsConf 2007 in Berlin, I decided that Erlang was going to be my challenge for 2008. However, the first months of 2008 passed without any Erlang until I listened to Joe Armstrong himself at Qcon in London. His talk was inspiring to me so right afterwards I bought his book and start reading it on my flight home.
After working myself through the first chapters, I finally got to the part that explained concurrent programming. In his book, he invites the reader to create an Erlang program that implements the Ring Benchmark. So I did and here is my version.
Problem
Create N processes in a ring. Send a message round the ring M times so that a total of N * M messages get sent. Time how long this takes for different values of N and M.
Solution
-module(ring_benchmark).
-export([start/2]).
%
% Create a ring of N processes through which a message is send M times.
% First, create a chain of processes: First,...,Last
% Then send a composed message to the first process containing:
% - throw, the type of request
% - the message (payload)
% - the process id of the last created process of the ring
% - number of times the message must be thrown through the ring.
%
% @author Ernest Micklei, ernestmicklei.com, May 2008
%
start(N,M) ->
First = spawn(fun() -> receive_send(void) end),
io:format("Created ~p~n",[First]),
Last = connect(First,N-1),
io:format("~p will send messages to ~p~n", [First,Last]),
First ! {throw,ball,Last,M},
done.
%
% Create a new process that will send messages to the process with ID = To
% Repeat this Count times, forming a chain (not a ring!) of processes.
% Return the process ID of the last created process.
%
connect(To,Count) ->
Next = spawn(fun() -> receive_send(To) end),
io:format("Created ~p~n",[Next]),
io:format("~p will send messages to ~p~n", [Next,To]),
case (Count =:= 1) of
true -> Next;
false -> connect(Next,Count-1)
end.
%
% Each process will exectue this in a loop.
% To is the process ID of the next process in the ring.
% If a throw request is received then send it to the ring.
% When the message is received, throw it again if the count > 0
% Else, send a die request to kill all processes of the ring.
%
receive_send(To) ->
receive
{Message,Ring,Count} ->
case (To =:= void) of
true ->
io:format("~p received ~p (~p)~n",[self(),Message,Count]),
case (Count =:= 0) of
true ->
{_,TimeAfter} = statistics(runtime),
io:format("Completed in ~p [ms]~n",[TimeAfter]),
Ring ! die;
false ->
Ring ! {Message,Ring,Count-1}
end,
receive_send(void);
false ->
io:format("~p received ~p (~p), sending it to: ~p~n",
[self(),Message,Count,To]),
To ! {Message,Ring,Count},
receive_send(To)
end;
{throw,Message,Ring,Count} ->
io:format("start throwing ~p ~p times through ring starting at: ~p~n",
[Message,Count,Ring]),
statistics(runtime),
Ring ! {Message,Ring,Count},
receive_send(To);
die ->
io:format("~p died~n",[self()]),
case (To =:= void) of
true -> void;
false -> To ! die
end
end.
If you are interested in numbers, N=10.000, M=10.000 on my Mac Book Pro took 101.290 milliseconds (runtime). I might update this post later after doing some more tests with different parameter sets.