EoF - Chapter 2 - Concurrency
Continuing on from yesterday with the Erlanging… Chapter 2 of the Erlang getting started documentation – Concurrent Programming.
(I realize that this is technically Chapter 3, but I’m not counting the introduction)
The chapter starts with little bit of background on what processes are and why they are useful. To paraphrase: processes are threads of execution that don’t share data/memory. Erlang processes pass messages between each other to allow for concurrency.
Creating a new process in Erlang is made super easy by using the built-in
function spawn(Module, Exported_Function, List of Arguments)
. For example, if
we wanted to spawn a process to run the function double/1
from the module
tut
from the last post,
we would write:
spawn(tut, double, [2])
Assuming tut.beam
(the module, as compiled by c(tut)
) is available in the
directory that we run this from, the process will execute double
and get the
return value 4
.
Unfortunately, we have no way of accessing this return value from the parent
process, as processes are memory independent! This is where sending messages
and the receive
construct come in.
Sending a Message
To send a message to a process, Erlang uses this pattern:
<PID> ! <message>.
Where <PID>
is the Process ID or Registered Name of the receiving process and
<message>
is any valid Erlang data type.
The return value of spawn
is the Process ID of the spawned process, and can
be used to send that process messages. A process can retrieve its own
Process ID by calling self()
.
The receiving process uses the receive
keyword to act accordingly on the
message it receives. This construct acts a bit like a switch statement,
executing the path that the message’s structure matches:
receive
<possible message pattern> ->
<executed code>;
<another pattern> ->
<executed code>
end.
Here’s a (really) brief and simple example of this in action. Pretend the module’s name is “helloer”:
receiver() ->
receive
say_hello ->
io:format("hello~n", []);
say_goodbye ->
io:format("goodbye~n", [])
end.
main() ->
Receiver_PID = spawn(helloer, receiver, []),
Receiver_PID ! say_hello,
Receiver_PID ! say_goodbye.
The console would look like this (starting with the call to main
):
1> helloer:main().
hello
say_goodbye
2>
This is a little weird. First, processes know where the “most intelligent”
place for output is, so even though the io:format
calls are in the spawned
process, they output to the current console.
The first line hello
comes from the spawned process running receiver
. The
second line, say_goodbye
, is the return value of main
(functions return
whatever their last value is). So where the hell is goodbye
?
The thing is, processes pause when they hit receive
until they receive a
message that matches one of the patterns. However, once they do receive a
matching message, they execute the corresponding actions, then return, and the
process ends. This means, that if we want receiver
to continue to await
messages, it needs to be recursive:
receiver() ->
receive
say_hello ->
io:format("hello~n", []);
say_goodbye ->
io:format("goodbye~n", [])
end,
receiver().
Because Erlang is tail-call optimized, this infinite recursion is essentially a while loop.
Messages can be any Erlang data structure, so if a message sender wants to include a “return stamp” for the receiver to send finished work back to, they can just include their own Process ID in the message:
PID ! {<data>, self()}.
Rather than tediously using a Process ID to refer to a process, a spawned process can be registered under an atom, and can be referred to by that atom module-wide:
register(name, spawn(module, function, [args])).
Communication Across Networks
Erlang has built-in distributed processing using a magic cookie. The
easiest way to accomplish this is by having a file named .erlang.cookie
with
permissions 400
(read permission only, only for owner) in the executing
user’s home directory on each computer. The file can contain anything as long
as it is identical in every location.
Using this method, I was able to easily send a message from my laptop to my Android phone on the same WiFi network by giving each Erlang node a “name”.
On laptop:
$ erl -name [email protected]
1> c(helloer).
{ok,helloer}
2> register(receiver, spawn(helloer, receiver, [])).
true
3>
On phone:
$ erl -name [email protected]
1> net_kernel:connect_node('[email protected]').
2> {receiver, '[email protected]'} ! say_hello.
Back on laptop:
hello
3>
Pretty neat!
The chapter ends with an extended example of using the above techniques to implement a simple Eshell messenger “app”. I highly recommend going through it yourself.
Next time, I’ll be going through the (somewhat shorter) chapter on Robustness. Later!
— M