Параллельное упражнение курса Erlang: Можно ли улучшить мой ответ? - PullRequest
5 голосов
/ 27 февраля 2011

Я выполняю это упражнение из курса erlang.org :

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?

  • Являюсь ли ядействительно завершается изящно?Требуется ли какое-либо специальное действие при завершении процесса, который был зарегистрирован?

Ответы [ 3 ]

6 голосов
/ 28 февраля 2011

Добро пожаловать в Эрланг!Надеюсь, вам понравится это так же, как и мне.

Есть ли какая-либо альтернатива указанию безусловного "истинного" предложения в последних двух операторах if?

Вы можете просто оставить это.Я запустил ваш код с этим:

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 и сделать еевсе немного чище.

Надеюсь, что это поможет, и удачи.

5 голосов
/ 28 февраля 2011

Пройдемся по коду:

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

Имя node я избегаю, потому что узел () в Erlang имеет коннотацию 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]))).

Эта функция плохая идея. Вам понадобится локальное имя, которое должно быть атомом, поэтому вы создали функцию, которая может создавать произвольные такие имена. Предупреждение здесь состоит в том, что таблица атомов не является сборщиком мусора и не ограничена, поэтому мы должны избегать этого, если это возможно. Хитрость для решения этой проблемы состоит в том, чтобы вместо этого пройти пиды и построить кольцо в обратном порядке. Окончательный процесс свяжет узел кольца:

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. Наконец, мы, вероятно, должны также связать начальный спавн, чтобы у нас не было большого живого кольца, но ни у кого нет ссылки на него. Возможно, мы могли бы зарегистрировать начальный процесс, чтобы потом было легче схватить кольцо.

Функция start также плоха в двух отношениях. Во-первых, мы должны использовать make_ref(), чтобы пометить уникальное сообщение и получить его, так что другой процесс не может быть зловещим и просто отправить done процессу запуска, пока работает кольцо. Вероятно, мы должны также добавить монитор на ринге, пока он работает. В противном случае мы никогда не будем проинформированы, должен произойти сбой звонка, пока мы ожидаем сообщение done (с тегом). Кстати, OTP выполняет оба синхронных вызова.


Наконец, наконец: нет, вам не нужно очищать регистрацию.

3 голосов
/ 28 февраля 2011

Мои коллеги сделали несколько отличных замечаний.Я также хотел бы упомянуть, что первоначальная цель проблемы избегается путем регистрации процессов, а не создания кольца.Вот одно из возможных решений:

-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.

Это решение использует записи.Если вы не знакомы с ними, прочитайте все о них здесь .

Каждый узел определяется функцией loop/2.Первое предложение loop/2 касается создания кольца (фаза сборки), а второе предложение касается печати сообщений (фаза данных).Обратите внимание, что все предложения заканчиваются вызовом loop/2, за исключением предложения rounds=0, которое указывает, что узел завершил свою задачу и должен умереть.Это то, что подразумевается под изящным прекращением.Также обратите внимание на хак, используемый, чтобы сообщить узлу, что он находится в фазе сборки - NextNode не pid, а целое число.

...