Erlang课程并发练习:我的答案可以改进吗?

时间:2011-02-27 19:07:01

标签: concurrency erlang

我正在从erlang.org course

进行此练习
  

2)写一个启动N的函数   一个环中的进程,并发送一个   所有的消息M次   环中的过程。之后   消息已经发送过程   应优雅地终止。

以下是我的想法:

-module(ring).
-export([start/2, node/2]).

node(NodeNumber, NumberOfNodes) ->
  NextNodeNumber = (NodeNumber + 1) rem NumberOfNodes,
  NextNodeName = node_name(NextNodeNumber),
  receive
    CircuitNumber ->
      io:format("Node ~p Circuit ~p~n", [NodeNumber, CircuitNumber]),
      LastNode = NodeNumber =:= NumberOfNodes - 1,
      NextCircuitNumber = case LastNode of
                           true ->
                             CircuitNumber - 1;
                           false ->
                             CircuitNumber
                         end,
      if
        NextCircuitNumber > 0 ->
          NextNodeName ! NextCircuitNumber;
        true ->
          ok
      end,
      if
        CircuitNumber > 1 ->
          node(NodeNumber, NumberOfNodes);
        true ->
          ok
      end
  end.

start(NumberOfNodes, NumberOfCircuits) ->
  lists:foreach(fun(NodeNumber) ->
                    register(node_name(NodeNumber),
                             spawn(ring, node, [NodeNumber, NumberOfNodes]))
                end,
                lists:seq(0, NumberOfNodes - 1)),
  node_name(0) ! NumberOfCircuits,
  ok.

node_name(NodeNumber) ->
  list_to_atom(lists:flatten(io_lib:format("node~w", [NodeNumber]))).

这是它的输出:

17> ring:start(3, 2).
Node 0 Circuit 2
ok
Node 1 Circuit 2
Node 2 Circuit 2
Node 0 Circuit 1
Node 1 Circuit 1
Node 2 Circuit 1

如果我真的知道Erlang,我可以做些不同的改进这段代码吗?特别是:

  • 在最后两个if语句中是否有指定do-nothing“true”子句的替代方法?

  • 我确实优雅地终止了吗?结束已注册的流程是否需要采取特殊措施?

3 个答案:

答案 0 :(得分:6)

欢迎来到Erlang!我希望你和我一样喜欢它。

  

在最后两个if语句中是否有指定do-nothing“true”子句的替代方法?

你可以放下这些。我用这个运行你的代码:

if NextCircuitNumber > 0 ->
  NextNodeName ! NextCircuitNumber
end,
if CircuitNumber > 1 ->
  node(NodeNumber, NumberOfNodes)
end

它对我有用。

  

我确实优雅地终止了吗?结束已注册的流程是否需要采取特殊措施?

是的,你是。您可以通过运行i().命令来验证这一点。这将显示进程列表,如果您的注册进程没有终止,您会看到许多已注册的进程遗留下来,如node0node1等。您也无法进行第二次运行程序,因为尝试注册已注册的名称会出错。

至于你可以做的其他改进代码的事情,没有太多因为你的代码基本上没问题。我可能做的一件事是取消NextNodeName变量。您只需将邮件直接发送到node_name(NextNodeNumber)即可。

此外,你可以做更多的模式匹配来改进。例如,我在使用您的代码时所做的一项更改是通过传入最后一个节点(NumberOfNodes - 1)的编号来生成流程,而不是传递NumberOfNodes。然后,我可以在我的node/2函数标题中模式匹配,如此

node(LastNode, LastNode) ->
    % Do things specific to the last node, like passing message back to node0
    % and decrementing the CircuitNumber
node(NodeNumber, LastNode) ->
    % Do things for every other node.

这使我能够清除case函数中的部分ifnode逻辑,并使其更加整洁。

希望有所帮助,祝你好运。

答案 1 :(得分:5)

让我们来看看代码:

-module(ring).
-export([start/2, node/2]).

名称node是我避免的名称,因为Erlang中的node()具有在某台机器上运行的Erlang VM的内涵 - 通常在多台机器上运行多个节点。我宁可称它为ring_proc或其他类似的东西。

node(NodeNumber, NumberOfNodes) ->
   NextNodeNumber = (NodeNumber + 1) rem NumberOfNodes,
   NextNodeName = node_name(NextNodeNumber),

这就是我们想要产生的东西,我们得到一个数字到下一个节点和下一个节点的名称。让我们看node_name/1作为插曲:

node_name(NodeNumber) ->
   list_to_atom(lists:flatten(io_lib:format("node~w", [NodeNumber]))).

这个功能不错。您将需要一个需要作为原子的本地名称,因此您创建了一个可以创建任意此类名称的函数。这里的警告是原子表不是垃圾收集和限制,所以我们应该尽可能避免它。解决这个问题的诀窍是改变pids并反向构建环。最后的过程将结束戒指的结:

mk_ring(N) ->
  Pid = spawn(fun() -> ring(none) end),
  mk_ring(N, Pid, Pid).

mk_ring(0, NextPid, Initiator) ->
   Initiator ! {set_next, NextPid},
   Initiator;
mk_ring(N, NextPid, Initiator) ->
   Pid = spawn(fun() -> ring(NextPid) end),
   mk_ring(N-1, Pid, Initiator).

然后我们可以重写你的启动功能:

start(NumberOfNodes, NumberOfCircuits) ->
  RingStart = mk_ring(NumberOfNodes)
  RingStart ! {operate, NumberOfCircuits, self()},
  receive
    done ->
        RingStart ! stop
  end,
  ok.

环代码就是这样的:

ring(NextPid) ->
  receive
    {set_next, Pid} ->
        ring(Pid);
    {operate, N, Who} ->
        ring_ping(N, NextPid),
        Who ! done,
        ring(NextPid);
    ping ->
        NextPid ! ping,
        ring(NextPid);
    stop ->
        NextPid ! stop,
        ok
  end.

并在戒指周围发射N次:

ring_ping(0, _Next) -> ok;
ring_ping(N, Next) ->
  Next ! ping
  receive
    ping ->
      ring_ping(N-1, Next)
  end.

(这些代码都没有经过测试,因此可能非常错误。)

至于你的其余代码:

receive
  CircuitNumber ->
    io:format("Node ~p Circuit ~p~n", [NodeNumber, CircuitNumber]),

我会使用某个原子CircuitNumber标记{run, CN}

  LastNode = NodeNumber =:= NumberOfNodes - 1,
  NextCircuitNumber = case LastNode of
                       true ->
                         CircuitNumber - 1;
                       false ->
                         CircuitNumber
                     end,

这可以通过if:

完成
  NextCN = if NodeNumber =:= NumberOfNodes - 1 -> CN -1;
              NodeNumber =/= NumberOfNodes - 1 -> CN
           end,

下一部分:

  if
    NextCircuitNumber > 0 ->
      NextNodeName ! NextCircuitNumber;
    true ->
      ok
  end,
  if
    CircuitNumber > 1 ->
      node(NodeNumber, NumberOfNodes);
    true ->
      ok
  end

确实需要true个案,除非你从未打过它。如果if中没有匹配项,则该过程将崩溃。通常可以重新编写代码,以便不依赖于计算结构,例如上面的矿井提示代码。


使用此代码可以避免一些问题。当前代码的一个问题是,如果环中的某些东西崩溃,它就会被破坏。我们可以使用spawn_link而不是spawn将环连接在一起,因此这些错误会破坏整个环。此外,如果在环运行时发送消息,我们的ring_ping函数将崩溃。这可以减轻,最简单的方法可能是改变环过程的状态,使其知道它当前正在运行并将ring_ping折叠成ring。最后,我们可能还应该链接最初的spawn,所以我们最终没有一个大的响铃,但是没有人参考。也许我们可以注册初始过程,以便以后轻松抓住戒指。

start功能在两个方面也很糟糕。首先,我们应该使用make_ref()来标记唯一的消息并接收标记,因此另一个进程不能是险恶的,只需在响铃工作时将done发送到启动进程。我们应该在环上添加监视器,同时它正在工作。否则我们将永远不会得到通知,在我们等待done消息(带标记)时应该响铃。顺便说一句,OTP同时进行同步调用。


最后,最后:不,你不必清理注册。

答案 2 :(得分:3)

我的同事们提出了一些很好的观点。我还想提一下,通过注册进程而不是实际创建一个环来避免问题的初始意图。这是一个可能的解决方案:

-module(ring).
-export([start/3]).
-record(message, {data, rounds, total_nodes, first_node}).

start(TotalNodes, Rounds, Data) ->
    FirstNode = spawn_link(fun() -> loop(1, 0) end),
    Message = #message{data=Data, rounds=Rounds, total_nodes=TotalNodes,
                       first_node=FirstNode},
    FirstNode ! Message, ok.

loop(Id, NextNode) when not is_pid(NextNode) ->
    receive
        M=#message{total_nodes=Total, first_node=First} when Id =:= Total ->
            First ! M,
            loop(Id, First);
        M=#message{} ->
            Next = spawn_link(fun() -> loop(Id+1, 0) end),
            Next ! M,
            loop(Id, Next)
    end;
loop(Id, NextNode) ->
    receive
        M=#message{rounds=0} ->
            io:format("node: ~w, stopping~n", [Id]),
            NextNode ! M;
        M=#message{data=D, rounds=R, total_nodes=Total} ->
            io:format("node: ~w, message: ~p~n", [Id, D]),
            if Id =:= Total -> NextNode ! M#message{rounds=R-1};
               Id =/= Total -> NextNode ! M
            end,
            loop(Id, NextNode)
    end.

此解决方案使用记录。如果您不熟悉它们,请阅读所有相关信息here

每个节点由loop/2函数定义。 loop/2的第一个子句涉及创建环(构建阶段),第二个子句涉及打印消息(数据阶段)。请注意,除了loop/2子句之外,所有子句都以对rounds=0的调用结束,这表示节点已完成其任务并应该死亡。这就是优雅终止的意思。还要注意用于告诉节点它处于构建阶段的hack - NextNode不是pid而是整数。