我正在从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”子句的替代方法?
我确实优雅地终止了吗?结束已注册的流程是否需要采取特殊措施?
答案 0 :(得分:6)
欢迎来到Erlang!我希望你和我一样喜欢它。
在最后两个if语句中是否有指定do-nothing“true”子句的替代方法?
你可以放下这些。我用这个运行你的代码:
if NextCircuitNumber > 0 ->
NextNodeName ! NextCircuitNumber
end,
if CircuitNumber > 1 ->
node(NodeNumber, NumberOfNodes)
end
它对我有用。
我确实优雅地终止了吗?结束已注册的流程是否需要采取特殊措施?
是的,你是。您可以通过运行i().
命令来验证这一点。这将显示进程列表,如果您的注册进程没有终止,您会看到许多已注册的进程遗留下来,如node0
,node1
等。您也无法进行第二次运行程序,因为尝试注册已注册的名称会出错。
至于你可以做的其他改进代码的事情,没有太多因为你的代码基本上没问题。我可能做的一件事是取消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
函数中的部分if
和node
逻辑,并使其更加整洁。
希望有所帮助,祝你好运。
答案 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而是整数。