5 votes

Exercice de concurrence du cours Erlang : Ma réponse peut-elle être améliorée ?

Je fais cet exercice à partir du cours erlang.org :

2) Ecrivez une fonction qui démarre N processus dans un anneau, et envoie un message M fois autour de tous les processus de l'anneau. Après l'envoi des messages ont été envoyés, les processus doivent se terminer de manière élégante.

Voici ce que j'ai trouvé :

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

Voici son résultat :

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

Si je connaissais vraiment Erlang, que pourrais-je faire différemment pour améliorer ce code ? Et plus spécifiquement :

  • Y a-t-il une autre solution que de spécifier une clause "true" sans effet dans les deux dernières instructions if ?

  • Suis-je vraiment en train de terminer avec élégance ? Une action spéciale est-elle nécessaire pour mettre fin à un processus qui a été enregistré ?

6voto

irritate Points 2047

Bienvenue à Erlang ! J'espère que vous l'apprécierez autant que moi.

Y a-t-il une autre solution que de spécifier une clause "true" sans effet dans les deux dernières instructions if ?

Vous pouvez les laisser de côté. J'ai exécuté votre code avec ceci :

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

et ça a marché pour moi.

Suis-je vraiment en train de terminer avec élégance ? Une action spéciale est-elle nécessaire pour mettre fin à un processus qui a été enregistré ?

Si, vous l'êtes. Vous pouvez le vérifier en exécutant la commande i(). commandement. Cela vous montrera la liste des processus, et si vos processus enregistrés ne se terminent pas, vous verrez qu'il reste beaucoup de vos processus enregistrés comme node0 , node1 etc. Vous ne pourrez pas non plus exécuter votre programme une deuxième fois, car il commettra une erreur en essayant d'enregistrer un nom déjà enregistré.

En ce qui concerne les autres choses que vous pourriez faire pour améliorer le code, il n'y a pas grand chose car votre code est fondamentalement bon. Une chose que je pourrais faire est de laisser de côté le NextNodeName variable. Vous pouvez simplement envoyer un message directement à node_name(NextNodeNumber) et ça marche.

De plus, vous pourriez probablement faire un peu plus de correspondance de motifs pour améliorer les choses. Par exemple, un changement que j'ai fait en jouant avec ton code était de lancer les processus en passant le numéro du dernier noeud. (NumberOfNodes - 1) plutôt que de passer le NumberOfNodes . Ensuite, j'ai pu faire une correspondance dans mon node/2 l'en-tête de la fonction comme ceci

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.

Cela m'a permis de nettoyer certains des case y if logique dans votre node et rendre le tout un peu plus ordonné.

J'espère que cela vous aidera, et bonne chance.

5voto

I GIVE CRAP ANSWERS Points 12429

Parcourons le code :

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

Le nom node est l'un de ceux que j'évite parce qu'un node() en Erlang a la connotation d'une VM Erlang fonctionnant sur une machine - généralement plusieurs nodes fonctionnant sur plusieurs machines. Je préfère l'appeler ring_proc ou quelque chose comme ça.

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

C'est ce que nous essayons de frayer, et nous obtenons un numéro vers le prochain nœud et le nom du prochain nœud. Regardons node_name/1 comme un interlude :

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

Cette fonction est une mauvaise idée. Vous aurez besoin d'un nom local qui doit être un atome, donc vous avez créé une fonction qui peut créer de tels noms arbitraires. L'avertissement ici est que la table des atomes n'est pas ramassée et limitée, donc nous devrions l'éviter si possible. L'astuce pour résoudre ce problème est de passer les pids à la place et de construire l'anneau en sens inverse. Le processus final fera alors le noeud de l'anneau :

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

Et ensuite nous pouvons réécrire votre fonction de départ :

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

Le code Ring est alors quelque chose du genre :

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.

Et pour tirer quelque chose autour de l'anneau N fois :

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

(Au passage, aucun de ces codes n'a été testé, il se peut donc qu'ils soient tout à fait faux).

Quant au reste de votre code :

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

Je marquerais le CircuitNumber avec un certain atome : {run, CN} .

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

Cela peut être fait avec un if :

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

La suite ici :

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

a besoin de la true cas, à moins que vous ne l'ayez jamais touché. Le processus s'arrêtera si rien ne correspond dans le champ if . Il est souvent possible de recâbler le code de manière à ne pas dépendre autant des constructions de comptage, comme le suggère mon code ci-dessus.


Plusieurs problèmes peuvent être évités avec ce code. Un des problèmes avec le code actuel est que si quelque chose s'écrase dans l'anneau, il est cassé. Nous pouvons utiliser spawn_link plutôt que spawn pour relier l'anneau ensemble, donc de telles erreurs détruiront l'anneau entier. En outre, notre ring_ping se plantera si on lui envoie un message pendant que l'anneau fonctionne. Il est possible d'y remédier, le moyen le plus simple étant probablement de modifier l'état du processus de l'anneau de manière à ce qu'il sache qu'il est en cours de fonctionnement et qu'il se replie sur lui-même. ring_ping en ring . Enfin, nous devrions probablement aussi lier le spawn initial afin de ne pas nous retrouver avec un grand nombre d'anneaux qui sont vivants mais auxquels personne n'a de référence. Nous pourrions peut-être enregistrer le processus initial afin qu'il soit facile de s'emparer de l'anneau par la suite.

Le site start est également mauvaise à deux égards. Premièrement, nous devrions utiliser make_ref() pour étiqueter un message unique et recevoir l'étiquette, de sorte qu'un autre processus ne peut pas être sinistre et envoyer simplement done au processus de démarrage pendant que l'anneau fonctionne. Nous devrions probablement aussi ajouter un moniteur sur l'anneau, pendant qu'il fonctionne. Sinon, nous ne serons jamais informés, si l'anneau tombe en panne alors que nous attendons la réponse de l'opérateur. done message (avec étiquette). OTP fait les deux dans ses appels synchrones d'ailleurs.


Enfin, enfin : Non, vous n'avez pas à nettoyer une inscription.

4voto

David Weldon Points 14329

Mes collègues ont fait d'excellents commentaires. J'aimerais également mentionner que l'intention initiale du problème est évitée en enregistrant les processus au lieu de créer réellement un anneau. Voici une solution possible :

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

Cette solution utilise des enregistrements. Si vous n'êtes pas familier avec eux, lisez tout à leur sujet ici .

Chaque nœud est défini par un loop/2 fonction. La première clause de loop/2 traite de la création de l'anneau (la phase de construction), et la deuxième clause traite de l'impression des messages (la phase de données). Remarquez que toutes les clauses se terminent par un appel à loop/2 sauf le rounds=0 qui indique que le nœud a terminé sa tâche et doit mourir. C'est ce que l'on entend par terminaison gracieuse. Notez également le hack utilisé pour dire au noeud qu'il est dans la phase de construction - NextNode n'est pas un pid mais plutôt un entier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X