Search code examples
erlangmonitoringspawn

Erlang: spawning process with monitor


I'm working through Joe Armstrong's Programming Erlang 2nd E. The book has exercises at the end of each chapter. Chapter 13, exercise 1 says:

Write a function my_spawn(Mod, Func, Args) that behaves like spawn(Mod, Func, Args) but with one difference. If the spawned process dies, a message should be printed saying why the process died and how long the process lived for before it died.

Here's a solution that has a race condition:

my_spawn(Mod, Func, Args) ->
    Pid = spawn(Mod, Func, Args),
    spawn(fun() ->
                  Ref = monitor(process, Pid),
                  T1 = erlang:monotonic_time(millisecond),
                  receive
                      {'DOWN', Ref, process, Pid, Why} ->
                          io:format("~p died because of ~p~n", [Pid, Why]),
                          io:format("~p lived for ~p ms~n", [Pid, erlang:monotonic_time(millisecond) - T1])
                  end
          end),
    Pid.

Spawning the process and creating the monitor is not an atomic step, so if the process dies after spawning but before the monitor is created, we won't get the error message.

Here's an attempt without the race condition:

my_spawn_atomic(Mod, Func, Args) ->
    spawn(fun() ->
                  {Pid, Ref} = spawn_monitor(Mod, Func, Args),
                  T1 = erlang:monotonic_time(millisecond),
                  receive {'DOWN', Ref, process, Pid, Why} ->
                          io:format("~p died because of ~p~n", [Pid, Why]),
                          io:format("~p lived for ~p ms~n", [Pid, erlang:monotonic_time(millisecond) - T1])
                  end
          end).

But the PID this returns is that of the monitoring process, not the Func process. And given that spawn always returns the PID of the process it creates, there doesn't seem to be a way to return Pid without resorting to a side effect.

What's the idiomatic way to do implement the atomic spawning?


Solution

  • You can send Pid from monitoring process as a message:

    my_spawn_atomic(Mod, Func, Args) ->
        Parent = self(),
        MPid = spawn(fun() ->
                      {Pid, Ref} = spawn_monitor(Mod, Func, Args),
                      Parent ! {spawned, self(), Pid},
                      T1 = erlang:monotonic_time(millisecond),
                      receive {'DOWN', Ref, process, Pid, Why} ->
                              io:format("~p died because of ~p~n", [Pid, Why]),
                              io:format("~p lived for ~p ms~n", [Pid, erlang:monotonic_time(millisecond) - T1])
                      end
              end),
        receive
            {spawned, MPid, Pid} -> Pid
        after 1000 -> error % 1s should be way enough for spawning monitoring process
        end.
    

    Another option is to wrap the function in a fun with an init phase:

    my_spawn(Mod, Func, Args) ->
        Pid = spawn(fun() ->
                receive run -> apply(Mod, Func, Args)
                after 1000 -> exit(init_timeout)
                end
            end),
        spawn(fun() ->
                      Ref = monitor(process, Pid),
                      T1 = erlang:monotonic_time(millisecond),
                      Pid ! run,
                      receive
                          {'DOWN', Ref, process, Pid, Why} ->
                              io:format("~p died because of ~p~n", [Pid, Why]),
                              io:format("~p lived for ~p ms~n", [Pid, erlang:monotonic_time(millisecond) - T1])
                      end
              end),
        Pid.