Using CPU Cores with Erlang

A few months ago, an article on Erlang in OFY reminded me that I had never got around to exploring Erlang. Erlang is fascinating because among other features, it was designed

  • to avoid catastrophic failure

  • to be able to scale to a system with a massive number of processes

  • to communicate using asynchronous messages between processes

These features make it natural for Erlang to use multiple cores of a processor. It also makes it easy to develop a distributed system.

You can get started with tutorials on the erlang.org site. Learning the syntax of a new language is not hard. Since it is a functional programming language, you may face a couple of challenges when moving from an imperative or object oriented language.

  • You need to forget about loops and iteration and think recursion. However, in Erlang, functions like foreach and map make it easy, especially if you are already familiar with map in Python.

  • You need to forget about variables, storing results in them and manipulating them. This was the harder one for me.

Concurrent Programming

Even if you have not done the tutorials, you should not find it hard to read Erlang code. So, consider a function, n_echo, which echoes a message N times. Write the following code in a text file, cp.erl:

% Module demos concurrent processes

-module(cp).
-export([n_echo/2]).

n_echo(Msg, 0) ->
  done;

n_echo(Msg,N) ->
  io:format("~s~n",[Msg]),
  n_echo(Msg, N-1).

The syntax of the format function is similar to the print function in Python 3, with ~ replacing \.

In the Erlang shell(erl command), type the following

1> c(cp).

2> cp:n_echo(one, 3).

3> cp:n_echo(two, 3).

As you would expect, you get one printed 3 times followed by two printed 3 times. In order to run these functions concurrently, you can spawn processes. Define a convenience function, Spawn_echo:

4> Spawn_echo = fun(Msg) ->

5> spawn(cp, n_echo, [Msg,3])

6> end.

The parameters of spawn are the module name, the function name and the list of parameters. Now, you can use this convenience function to spawn a list of processes:

7> lists:foreach(Spawn_echo, [one, two, three]).

The foreach function will apply Spawn_echo function to each element of the list. Now, you will find that one, two and three are printed 3 times each but running concurrently. Programming concurrency in Erlang is about as easy as it can be.

Now, consider the example of two programs, ping and pong. Ping sends a message to pong and waits for a reply. Pong is waiting for a message and responds after receiving a message. Add these methods to the cp.erl file and modify the export line as follows:

-export([n_echo/2, ping/3, pong/0, ping_pong/0]).

ping(0, Pong_PID,Ping) ->
  Pong_PID ! quit,
  io:format("ping finished~n", []);

ping(N, Pong_PID,Ping) ->
  Pong_PID! {Ping, self()},
  receive
  X ->
    io:format("Ping received ~w~n",[X])
  end,
  ping(N-1, Pong_PID, Ping).

Pong() ->
receive
  quit ->
   io:format("Pong got quit~n",[]);
  {X , Ping_PID} ->
   io:format("Pong received ~w~n",[X]),
   Ping_PID ! [X,pong],
   pong()
  end.

ping_pong() ->
  Pong_PID = spawn(cp, pong, []),
  spawn(cp,ping, [3, Pong_PID,ping]).

Note how easy it is to send a message – Process ID ! <data>. Data can be an atom, a tuple, a list, etc. The processing of a receive statement is very much like a case statement. Depending upon what is received, the appropriate code is executed.

The ping_pong function spawns both processes. Pong is started first and its process id is passed to ping. Once ping starts, it sends a message to pong along with its process id.

Now, from Erlang shell,

8> c(cp).

9> cp:ping_pong().
Pong received ping
Ping received [ping,pong]
...

In ping_pong, spawn twice as follows:

spawn(cp,ping, [3, Pong_PID,ping]),
spawn(cp,ping, [3, Pong_PID,ping_ping]).

You should alter pong so that it does not die after receiving a quit message:

quit ->
  io:format("Pong got quit~n",[]),
  pong();

Now compile cp and run ping_pong again. You can notice that you have the structure of a client/server program.

However, if the ping processes need to be run at various occasions, it may not be possible to have the process id of the pong process. Erlang provides an option to register a process. Other processes can then use the symbolic reference. You will alter the ping_pong function as follows:

ping_pong() ->
  register(pong, spawn(cp, pong, [])),
  spawn(cp, ping, [3, ping]),
  spawn(cp, ping, [3, ping_ping]).

Notice that you no longer pass the process id of pong to ping. Ping will use the symbolic name pong to send a message. The changes to the export statement and the ping function will be as follows:

-export([n_echo/2, ping/2, pong/0, ping_pong/0]).

ping(0, Ping) ->
  pong ! Quit,
  io:format("~w finished~n", [Ping]);

ping(N, Ping) ->
  pong ! {Ping, self()},
  receive
  X ->
  io:format("~w received ~w~n",[Ping,X])
  end,
  ping(N-1, Ping).

With this brief exploration, you should be convinced that Erlang is an excellent option for writing code to exploit multiple cores of the current processors and will try it out. Learning a new language is fun!

Nov 2013

Comments