Concurrency
Processes
- Forking
Message passing
- Synchronization and naming conventions
- Erlang approach
Reacting to multiple messages
- From the same origin as well as different sources
More sequential constructions
- Exceptions
- List comprehensions
- Lambda expressions
- Higher-order functions
Processes
Process is a sequential program that can send and receive messages to/from other processes.
Each process has a mailbox (message queue)
No shared data!
- Each process owns all values that it has created or received from other processes
- Isolation
- Implementation: each process has its own heap (what are the performance implications?)
- Different from threads
Sending messages
Proc!Msg
putsMsg
into the mailbox ofProc
(identified by process id or registered name)- Sending is asynchronous
Receiving messages
receive X1 -> E1; X2 -> E2; ... end
looks at each message from the mailbox (starting from the oldest one) and tries to match it against each of the patterns- The oldest message that matches any of the patterns is removed from the mailbox, and the relevant expression becomes the value of the receive statement
- If no message matches any of the patterns, the process will be stuck waiting for a message that matches
- Receive statement may have guards
Spawning processes
- Builtin function
spawn()
takes a function as argument and creates a new process that starts executing that function
start() -> Pid = spawn(fun run/0), io:format("Spawned ~p~n",[Pid]). run() -> io:format("Hello!~n",[]).
- Expression
fun run/0
indicates to execute the functionrun
which has zero arguments. - Function
spawn
returns the proceed Id (Pid
) of the newly created process for future interaction with it. - Function
self()
returng the PID of the current process
- Builtin function
Passing a value
test1(N) -> P = self(), Pid = spawn(messages, run, [P]), io:format("Spawned ~p~n",[Pid]), io:format("Sending ~p~n",[N]), Pid!{msg1, N}, receive {msg2, M} -> io:format("Received back ~p~n",[M]) end. run(P) -> io:format("Hello!~n"), receive {msg1, N} -> io:format("Received ~p~n",[N]), io:format("Sending back ~p~n",[N+1]), P!{msg2, N+1} end.
spawn(messages, run, [P])
is another variant ofspawn()
, which takes the name of the module, the name of the function, and the arguments to the function- The function must be exported by the module
Functions are part of the
erlang
module (more here)
Mailboxes (message queues)
Erlang has the ability to "listen" for messages from different senders
In which order will they be process?
Can we force an order?
The semantics for a
receive
statementA receive statement tries to find a match as early in the mailbox as it can
receive msg3 -> 42 end
Another example
receive msg2 -> 42 end
Waiting for multiple messages
receive msg2 -> 42; _ -> 41 end
The
_
matches with any message in the mailbox.
- The oldest message is tried against every pattern of the receive until one of them matches.
A simple echo-server
Interaction
Process A sends a token to a Process B
Process B just returns it back to Process A
-module(echo). -export([start/0, echo/0]). echo() -> receive {From, Msg} -> From ! {self(), Msg}, echo(); stop -> true end. start() -> Pid = spawn(fun echo/0), % sending tokens to the server Token = 42, Pid ! {self(), Token}, io:format("Sent~w~n",[Token]), receive {Pid, Msg} -> io:format("Received ~w~n", [Msg]) end, % stop server Pid ! stop.
Observe that Process A waits only for a message from Process B (
{Pid, Msg}
, wherePid
is bound)Servers are implemented using tail recursion
Client-Server Architecture
Common asynchronous communication pattern
- For example: a web server handles requests
for web pages from clients (web browsers)
- For example: a web server handles requests
Mathematical server
Off-loading heavy mathematical operations
- For example: factorial
Server code
loop(Count) -> receive stop -> true ; {get_count, From, Ref} -> From ! {result, Ref, Count}, loop(Count); {factorial, From, Ref, N} -> Result = factorial(N), From ! {result, Ref, Result}, loop(Count+1) end. % starting server with initial state 0 start() -> spawn(fun() -> loop(0) end).
Client code
compute_factorial(Pid, N) -> Ref = make_ref(), Pid ! {factorial, self(), Ref, N}, receive {result, Ref, Result} -> Result end.
A simple test
> c(math_examples). {ok,math_examples} > math_examples:factorial(10). 3628800 > mserver:start(). <0.40.0> > mserver:compute_factorial(list_to_pid("<0.40.0>"),10). 3628800
What if the server crashes or stop?
> list_to_pid("<0.40.0>") ! stop > mserver:compute_factorial(list_to_pid("<0.40.0>"),10). * 2: syntax error before: mserver > mserver:start(). <0.44.0> > mserver:compute_factorial(list_to_pid("<0.44.0>"),10). 3628800
Processes IDs are dynamic but code is static!
- Change in a process ID might lead to notify all the potential clients
Sources of multiple messages
Multiple messages can come from different processes (code)
-module(echo2). -export([start/0, echo/1]). % New processes need to seed the random number generator % as all of them start with the same RNG state. echo(Seed) -> random:seed(Seed), echo(). echo() -> receive {From, Msg} -> timer:sleep(random:uniform(100)), From ! {self(), Msg}, echo(); stop -> true end. start() -> SeedB = random:uniform(100), SeedC = random:uniform(100), PidB = spawn(echo2, echo, [SeedB]), PidC = spawn(echo2, echo, [SeedC]), % sending tokens Token = 42, PidB ! {self(), Token}, io:format("Sent ~w~n",[Token]), Token2 = 7, PidC ! {self(), Token2}, io:format("Sent ~w~n",[Token2]), % receive messages receive {PidB, Msg} -> io:format("Received from B: ~w~n", [Msg]) ; {PidC, Msg} -> io:format("Received from C: ~w~n", [Msg]) end, receive {PidB, Msg2} -> io:format("Received from B: ~w~n", [Msg2]) ; {PidC, Msg2} -> io:format("Received from C: ~w~n", [Msg2]) end, % stop echo-servers PidB ! stop, PidC ! stop.
Function
timer:sleep(N)
sleeps a process forN
millisecondsFunction
random:uniform(N)
produces a random integer between1
andN
Distinguish the source by
Pid
s
Multiple messages can come from the same processes (code)
Erlang has asynchronous messages
Send several messages of the same shape and continue computing
When receiving the responses, how can the code match them to the appropriate request?
-module(echo3). -export([start/0]). echo() -> receive {From, Msg} -> From ! {self(), Msg}, echo(); stop -> true end. start() -> PidB = spawn(fun echo/0), % sending tokens Token = 42, PidB ! {self(), Token}, io:format("Sent~w~n",[Token]), Token2 = 41, PidB ! {self(), Token2}, io:format("Sent~w~n",[Token2]), % receive messages receive {PidB, Msg} -> io:format("Received 41? ~w~n", [Msg]) ; {PidB, Msg} -> io:format("Received 42? ~w~n", [Msg]) end, % stop echo-servers PidB ! stop.
BIF
make_ref
provides globally unique reference objects (references for short) different from every other object in the Erlang system including remote nodesReferences can be used to uniquely identify messages! (code)
-module(echo4). -export([start/0]). echo() -> receive {From, Ref, Msg} -> From ! {self(), Ref, Msg}, echo(); stop -> true end. start() -> PidB = spawn(fun echo/0), % sending tokens Token = 42, Ref = make_ref(), PidB ! {self(), Ref, Token}, io:format("Sent~w~n",[Token]), Token2 = 41, Ref2 = make_ref(), PidB ! {self(), Token2}, io:format("Sent~w~n",[Token2]), % receive messages receive {PidB, Ref2, Msg} -> io:format("Received 41? ~w~n", [Msg]) ; {PidB, Ref, Msg} -> io:format("Received 42? ~w~n", [Msg]) end, % stop echo-servers PidB ! stop.
Registered processes
Erlang has a method for publishing a process identifier
- Any other process can communicate with it
BIF
register
% starting server with initial state 0 start() -> Pid = spawn(fun() -> loop(0) end), register(server,Pid).
The atom
server
can be used instead of a concrete process ID> mserver2:start(). true > mserver2:compute_factorial(server,10). 3628800 > server ! stop. stop > mserver2:compute_factorial(server,10). ** exception error: bad argument in function mserver2:compute_factorial/2 > mserver2:start(). true > mserver2:compute_factorial(server,10). 3628800
Distributed environments
Message passing abstractions extend easily for distributed environments
Erlang nodes
An instance of an Erlang runtime system
Nodes can easily communicate with each other
Creating a node
erl -name 'nodeS@127.0.0.1' -setcookie lecture
- The cookie provides security (not everyone can connect)
- The name reflects the node's IP address
Creating two nodes (for simplicity on the same machine)
erl -name 'nodeS@127.0.0.1' -setcookie lecture erl -name 'nodeC@127.0.0.1' -setcookie lecture
Connecting nodes
- From
nodeC@127.0.0.1
(nodeC@127.0.0.1)> net_adm:ping('nodeS@127.0.0.1'). pong (nodeC@127.0.0.1)> nodes(). ['nodeS@127.0.0.1']
- From
Distributed mathematical server
Running your code
- Send the compiled version of your code to the connected nodes
(nodeC@127.0.0.1)> nl(math_examples). abcast (nodeC@127.0.0.1)> nl(mserver2). abcast
- The server gets started on the
nodeS
node
(nodeS@127.0.0.1)> mserver2:start(). true
- The client communicates with the server
(nodeC@127.0.0.1)> mserver2:compute_factorial({server, 'nodeS@127.0.0.1'}, 10). 3628800
Use of
{registered_name, node@IP}
instead of the process ID (e.g.<0.47.0>
) or only the registered name (e.g.server
)The code has not been changed for running in a distributed setting!
Forking
Spawning new process is also known as forking
This terminology comes from the operating system community
Look at the picture upside-down and you got a fork!
Message passing
So far we considered synchronisation mechanisms based on shared variables
- Concurrent programs require hardware in which processors share memory
What about networked (distributed) architectures?
No shared memory
Message passing is the natural model for such scenarios
Words by Joe Armstrong (Programming Erlang - Software for a concurrent world)
The programming world went one way (toward shared state). The Erlang community went the other way. (Few other languages followed the “message passing concurrency” road. Others were Oz and Occam.)
We don’t have shared memory. I have my memory. You have yours. We have two brains, one each. They are not joined together. To change your memory, I send you a message: I talk, or I wave my arms. You listen, you see, and your memory changes;
It is relatively easy to adapt Erlang programs for distributed environments
How does it work?
One process sends a message
Another process awaits for a message
Dimensions for message passing approaches:
What form of synchronisation is required
What form of process naming is involved in message passing
Forms of synchronization
Consider the behaviour of the sender of a message
Asynchronous send
- Send and continue working (e-mail, SMS)
Synchronous send
- Send and wait for the message to be received (fax)
Rendezvous / Remote invocation
- Send and wait for reply (phone call)
Examples
Erlang has asynchronous message passing
Ada has rendezvous
- Previously used at Chalmers as a main teaching language
Java has libraries
Sockets – asynchronous message passing
Remote Method Invocation – it can be seen as synchronous/rendezvous message passing
Forms of naming
How do sender and receiver refer to each other when message passing is used?
Direct symmetric
Direct asymmetric
- Erlang has this naming convention
- Sockets
Indirect
Naming a process is not always convenient
Naming an intermediary can be more flexible
A channel, service name, or a mailbox
Potentially many-to-many communication
Erlang message passing
Asynchronous send
Receive from a mailbox
Exceptions
Exceptions work similar as in Java
1> (1/0) + 2. ** exception error: an error occurred when evaluating an arithmetic expression in operator '/'/2 called as 1 / 0
Exceptions (errors) can be regarded as special values
An error value poisons the computation
(1/0) + 2
→
(error) + 2
→
(error)
catch
constructions limit the poisoning2> io:format("value: ~p~n", [1/0]). ** exception error: an error occurred when evaluating an arithmetic expression in operator '/'/2 called as 1 / 0
io:format("value: ~p~n", [1/0])
→
io:format("value: ~p~n", [(error)])
→
io:format("value: ~p~n", (error))
→
(error)
3> io:format("value: ~p~n", [catch (1/0)]). value: {'EXIT',{badarith,[{erlang,'/',[1,0],[]},
{erl_eval,do_apply,6, [{file,"erl_eval.erl"},{line,673}]}, {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,431}]}, {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,228}]}, {erl_eval,expr_list,6, [{file,"erl_eval.erl"},{line,877}]}, {erl_eval,expr,5,[{file,"erl_eval.erl"},{line,404}]}, {shell,exprs,7,[{file,"shell.erl"},{line,686}]}, {shell,eval_exprs,7, [{file,"shell.erl"},{line,641}]}]}}
ok
io:format("value: ~p~n", [catch (1/0)])
→
io:format("value: ~p~n", [catch (error)])
→
io:format("value: ~p~n", [(error representation)])
→
io:format("value: ~p~n", [(error representation)])
→
ok
catch
exists in two variantstry ... catch ... after ...
(like try-catch-finally in Java)catch
(simple version)
Three constructions for errors exist
error()
(default)throw()
(when the same code throws and catches exceptions used for flow-control)exit()
(typically used for exiting servers)
One more kind of exceptions exists when process is killed — we will talk about it later
List comprehensions
1> [ X + 2 || X <- [2, 4, 5]]. [4,6,7]
2> [ X + Y * 10 || X <- [2, 4, 5], Y <- [1, 4]]. [12,42,14,44,15,45]
- List comprehensions contain one or more generators (
X <- [2, 4, 5]
), an expression (X + 2
), and optionally filters
3> [ X + 2 || X <- [2, 4, 5], X > 3]. [6,7]
- Simple loops can be implemented using list comprehensions
4> [ io:format("~p~n", [X]) || X <- [apple, pear, orange]]. apple pear orange [ok,ok,ok]
- Generators can depend only on (results of) earlier generators
5> [ {X, Y} || X <- [2, 3, 4], Y <- lists:seq(1, X)]. [{2,1},{2,2},{3,1},{3,2},{3,3},{4,1},{4,2},{4,3},{4,4}]
6> [ {X, Y} || Y <- lists:seq(1, X), X <- [2, 3, 4]]. * 1: variable 'X' is unbound
- Filters also can only refer to results of earlier generators
7> [ {X, Y} || X <- [2, 3, 4], Y <- lists:seq(1, X), Y > 1]. [{2,2},{3,2},{3,3},{4,2},{4,3},{4,4}]
8> [ {X, Y} || X <- [2, 3, 4], Y > 1, Y <- lists:seq(1, X)]. * 1: variable 'Y' is unbound
Lambda expressions
- Creating a new process requires defining a function
f() -> io:format("new process created!~n"). test() -> spawn(fun f/0).
- It is possible to define an anonymous function by using a lambda expression
f() -> io:format("new process created!~n"). test() -> spawn(fun () -> io:format("new process created!~n") end).
Higher-order functions
Functions that take other functions as arguments (like
spawn
) are called higher-order functions (HOFs)HOFs are a very useful way of generalizing code
sum([]) -> 0; sum([X|Xs]) -> X + sum(Xs). mul([]) -> 1; mul([X|Xs]) -> X * mul(Xs).
- Since the pattern of computation is the same for both
sum
andmul
, we can create a common definition for both
reduce(_, E, []) -> E; reduce(F, E, [X|Xs]) -> F (X, reduce(F, E, Xs)). sum2(L) -> reduce(fun(X, Y) -> X + Y end, 0, L). mul2(L) -> reduce(fun(X, Y) -> X * Y end, 1, L).
reduce/3
exists aslists:foldr/3
We can also use
reduce/3
to implementappend/2
(concatenation of two lists)
append(L1, L2) -> reduce(fun(X, Y) -> [X|Y] end, L2, L1).
Functions like
sum
andmul
, where the binary operator is associative can be implemented using tail recursion efficienlyappend/2
, on the other hand, cannot beFor
sum
andmul
one should uselists:foldl/3
, which is tail-recursive
Begin-end blocks
- Begin-end blocks can be used to put a sequence of statements where comma means something else
f(a, b, begin io:format("hello~n"), c end)