I am new to erlang and starting tutorial from 'Programming Erlang' by Joe Armstrong.
I feel puzzled about the 'save queue' mentioned in the selective receive in 8.6. If the message is not matched at all, why not drop it directly? What's the purpose of putting it back to the mailbox for later processing? If it's default behavior, those garbage messages (meaning that they cannot be matched) could lead to performance penalties as they will accumulate without release.
I would like to know whether I misunderstood this part. I tried to inspect the contents of a process mailbox to get a better understanding but I was unable to.
Please help, I would appreciate any code snippet that proves a point, thanks.
This paragraph of the book describes the details of what is done by one receive bloc. Don't forget that it is possible to have several receive blocs processed sequentially by one single process, so one message which doesn't match any entry of the first receive bloc will be put in the save queue (for efficiency) because:
- it will never match any entry of the current receive bloc,
- the current receive bloc is guaranteed to finish only when one entering message matches one entry or the timeout ends.
When one receive bloc is done, then the save queue is put back, in original receive order, in the mail box because the next receive bloc has a chance to have an entry which match one of the "save queued" messages.
One usage is to manage priorities:
loop() ->
...
receive
{high_prio,Msg} -> process_priority_message(Msg)
after 0
ok % no priority message
end,
receive
{low_prio,Msg} -> process_normal_message(Msg);
Other -> process_unexpected_message(Other)
end,
...
loop()
This code allow to process a message {high_prio,Msg} even if it is not in the first position within the message queue.
And you are right, there is a risk that unexpected messages accumulate in the mail box, especially in a never ending loop, it is why you will see very often something like the last line
Other -> process_unexpected_message(Other)
to empty the mail box.
It is an enormous help when building concurrent systems as it allows you to only concentrate on those messages in which you are interested at that point and ignore other messages. Erlang systems are typically non-deterministic so you seldom know what you are going to receive and when. Without the automatic save queue it would mean that at every point you receive a message you would have to be able to handle every message that could possibly arrive at this process. It very quickly becomes a combinatorial explosion of states and messages.
As an example take simple server. At its top-level there will be a receive loop which receives requests for the server to handle. It will then process the first request. Most likely during this processing it will be communicating with other processes, and receiving messages. While processing a request a new request message can arrive at the server. If Erlang didn't save the messages then you would have to handle these requests everywhere in the server code where messages are received. Now you can ignore these new requests and leave them for the top-loop which should handle them.
Keeping track of all messages that need to be handled somewhere in your server quickly becomes unworkable. For example in a gen_server
you have the actual requests sent by clients (message format unspecified), "system" messages to the server (message format unspecified), any number of well-defined messages that are meaningful to the server code in addition to all the messages your processing of requests needs.
You would end up implementing your own message buffer and passing it around.
Not having the message save queue would make it practically impossible to write generic modules which send/receive messages while processing, for example the client functions to gen_server
s. They would have to know about every message that could arrive at that process and needs to be handled.
Yes, saving all messages can be a problem, but it is usually the type of problem which is solvable when you are aware of it. For example in the top-loop of a server you can be reasonably certain that an unknown message can be received and thrown away. A gen_server
does receive all messages at it top-level where it knows what to do with them, some it processes itself (the system messages) and the others it passes on the specific server code.
And it allows you to easily handle priority messages as @Pascal showed.
+1 to rvirding, "It is an enormous help when building concurrent systems" is exactly what it is about. Reminded me of some example code - a "Smokers Problem" solution - I cranked up a while back. I thought sharing it might help to illustrate the point:
-module(smokers).
-export([smokers/0]).
smokers() ->
Rounds = 1000000,
Agent = self(),
lists:foreach(fun(Material) -> spawn(fun() -> startSmoker(Agent, Material) end) end, materials()),
done = agent(Rounds),
io:format("Done ~p rounds~n", [Rounds]).
agent(0) ->
done;
agent(Rounds) ->
offer(twoRandomMaterials(), Rounds).
offer(AvailableMaterials, Rounds) ->
receive
{take, Smoker, AvailableMaterials} ->
Smoker ! smoke,
receive
doneSmoking ->
agent(Rounds - 1)
end
end.
startSmoker(Agent, Material) ->
smoker(Agent, lists:delete(Material, materials())).
smoker(Agent, Missing) ->
Agent ! {take, self(), Missing},
receive
smoke ->
Agent ! doneSmoking,
smoker(Agent, Missing)
end.
twoRandomMaterials() ->
Materials = materials(),
deleteAt(random:uniform(length(Materials)) - 1, Materials).
materials() ->
[paper, tobacco, match].
deleteAt(_, []) -> [];
deleteAt(0, [_ | T]) -> T;
deleteAt(Idx, [H | T]) -> [H | deleteAt(Idx - 1, T)].
The interesting thing here is that there are potentially three messages in the queue when we try the receive {take, Smoker, AvailableMaterials}
. Only one of them can however be processed now. Then there is the inner receive doneSmoking
as a handshake. So for one the selection of the proper messages that allows the code to do some work at all as well as not loosing the other take
messages when receiving the handshake message is what solves all the concurrency issues here. Were the non-matching/non-processable messages dropped at any point, the smoker
s would get stuck forever (unless they would periodically repeat their requests).