Friday, November 7, 2008

On the "Problem 8.11"

Joe Armstrong suggests to solve the following problem (8.10.1) in his book Programming Erlang:
"Write a function start(AnAtom, Fun) to register AnAtom as spawn(Fun). Make sure that your program works correctly in the case when two parallel processes simultaneously evaluate start/2. In this case you must guarantee that one of these processes succeeds and the other fails."
The solution proposed by Ladislav Lenart and slightly modified by Alain O'Dea included try->catch mechanism attempts to call the register(AnAtom, ...) concurrently, inside the try clause. The hope is that the "second" call will fire an exception which is being caught later:
-module (start_register).
-export ([start/2]).

start(Atom, Fun) ->
Registrant = self(),
spawn(
fun() ->
try register(Atom, self()) of
true ->
Registrant ! true,
Fun()
catch
error:badarg ->
Registrant ! false
end
end),
receive
true -> true;
false -> erlang:error(badarg)
end.

Does this work? I tried to use it manually and it did. But I could not simulate all possible situations which could happen when a lot of processes will try to bump upon this line. And even if I could, there is no guarantee something else won't happen which was not tested. Well, that is what exception mechanism is for, but...
but why bother? We have the full power of Erlang specifically designed to take care of concurrency problems!
Instead of performing "suspicious" actions I would "eliminate" concurrency here at all, moving it to the Erlang process mailbox. By allowing only a single dedicated process to register atoms. It is registered itself as registrar and serves requests to register other funs. Basically, I am using the Erlang's response to concurrency: "GET IN LINE!", meaning that the mechanism of storing messages in the process mailbox is very well tuned:
-module(prob8101).
-export([start/2,init_registrar/0]).

init_registrar() ->
register(registrar,spawn(fun() -> loop() end)).

loop() ->
receive
{register,Atom,Fun,From} ->
case whereis(Atom) of
undefined ->
register(Atom,spawn(Fun)),
From!{registered_ok,Atom,self()},
loop();
_Otherwise ->
From!{already_registered,Atom,self()},
loop()
end
end.

start(AnAtom, Fun) ->
registrar ! {register, AnAtom, Fun, self()},
Rid = whereis(registrar),
receive
{Result,AnAtom,Rid} -> Result
end.


Here we have only one process calling register(...) and we won't have any troubles with concurrent requests for there are none possible.

Here is the sample session:

~/Documents/erlang myacct$ erl
Erlang (BEAM) emulator version 5.5.4 [source]
[async-threads:0] [kernel-poll:false]

Eshell V5.5.4 (abort with ^G)
1> c(prob8101).
{ok,prob8101}
2> prob8101:init_registrar().
true
3> prob8101:start(bbb,fun() ->
receive {From,A} -> From!{reply,A} end end).
{bbb,'is undefined'}
registered_ok
4> prob8101:start(bbb,fun() ->
receive {From,A} -> From!{reply,A} end end).
{bbb,'is defined',<0.40.0>}
was_registered
5>

2 comments:

Roach said...

I know this is an old post but having found it trying to verify my solution to the problem I wanted to drop you a line and say thanks.

The question says to register AnAtom as spawn(Fun) - the solution you quote from nabble doesn't do that. It registers the calling process using self().

If you were to attempt to send a message to AnAtom after doing this, it most certainly wouldn't be delivered to the right mailbox.

After finding the original solution you quote and many others like it, I was starting to think I was not understanding the question and my solution (very much the same as yours) was way off.

Serge said...

Thank you for the insight. You are absolutely right - that code as a whole seems incorrect.

So I have posted an updated code to illustrate the whole picture.