IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Caml Discussion :

La "bonne façon" de faire du parallelisme


Sujet :

Caml

  1. #1
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut La "bonne façon" de faire du parallelisme
    Salut,

    Pour me remettre à OCaml après un passage par Python, j'ai décidé de reprendre un projet que j'avais abandonné.

    Seulement quand j'avais commencé ce projet je l'avais fait avec les pieds, notamment pour ce qui est de la gestion de la concurrence. Donc je me suis dit, autant apprendre à faire ça correctement.

    En gros, une des actions que j'ai le plus souvent besoin de faire c'est de lancer des algos depuis l'interface graphique. L'interface graphique ne doit pas freezer et il doit être possible de lancer plusieurs algos en même temps, ainsi que d'arrêter des algos en cours d'exécution depuis l'interface graphique.

    Je précise que je n'ai aucune connaissance de tout ce qui est programmation
    système, encore moins en OCaml (j'avais fini par apprendre à faire quelques trucs en Python mais c'était surtout de la manipulation de boîtes noires). L'ensemble de mes connaissances vient de ce document, que j'ai découvert il y a quelques jours.

    Après l'avoir lu, il m'a semblé une façon de faire pourrait être de :

    - Faire tourner l'interface graphique dans le processus principal.
    - Lancer dans des algos dans d'autres processus plutôt que dans des threads.
    Certes, il y aura le problème du coup de la copie de l'environnement et du transfert des données, mais si j'ai bien compris ça devrait me permettre d'exploiter plusieurs coeurs (enfin, le jour où j'aurai un PC multicore...). Et la gestion de la concurrence sera simplifiée.
    - Ensuite, le processus parent doit attendre le résultat du processus fils, pour pouvoir ensuite modifier l'environnement en conséquence. Le problème, c'est que je ne veux pas freezer mon interface graphique. Du coup, je me suis dit que je pourrais le faire retourner dans le thread principal (ainsi, il retombe sur la event loop de Tk), et le faire attendre dans un thread, dont le seul but sera d'attendre le résultat, de le lire depuis le pipe, puis d'actualiser l'environnement en fonction de ce résultat.

    J'ai résumé ça sur un diagramme :



    Ma question est simple : Est-ce la (ou a défaut une) bonne façon de procéder ?

    Plus précisément :

    - Est-ce que l'utilisation d'un processus (plutôt qu'un thread) pour faire tourner l'algorithme est une bonne idée ?
    - Est-ce que le transfert du résultat du child vers le parent via un pipe plus de la sérialization est une bonne idée ?
    - Est-ce que le fait de faire attendre le résultat dans un thread secondaire de manière à faire retomber le thread principal sur Tk.mainLoop est une bonne idée ?
    - Même si c'étaient de bonnes idées, y en a-t-il des meilleures ?

    Bon puis après il y aura la question de l'implémentation. Pour l'instant j'ai essayé de faire un truc mais ça ne marche pas. J'attends vos conseils sur les modifications à faire avant de reprendre mon code et de le poster...

    Merci d'avance !

  2. #2
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut
    J'ai l'impression que ce n'est pas la bonne façon de faire, en partie parce que je n'arrive pas à l'implémenter. Mes différents codes compilent mais lors de l'exécution, ça freeze puis ça finit par m'afficher un message du genre :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    [xcb] Unknown sequence number while processing queue
    [xcb] Most likely this is a multi-threaded client and XInitThreads has not been called
    [xcb] Aborting, sorry about that.
    etc...
    En recherchant l'erreur sur le net, je suis tombé sur des liens comme celui-ci

    Où il est dit :

    Citation Envoyé par Laurent
    Your first code is not supposed to work: a window can only be active in one thread at the same time. Note that calling Clear/Draw/Display implicitely tries to activate the window.
    Ici il n'est pas dit explicitement que Tk est soumis à cette limitation, mais effectivement quand je fais des tests avec des threads, dès que je rajoute du Tk dans le coup, ça ne marche plus comme attendu.

    Un exemple (un peu moche)

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    let f a =
      let lim = if a = "parent" then 100000000 else 10000000 in
      print_string ("début " ^ a); print_newline ();
      for k = 1 to lim do () done;
      print_string ("fin " ^ a); print_newline ()
    ;;
     
    let fen = Tk.openTk () ;;
    let txt = Textvariable.create ();;
    Textvariable.set txt " " ;;
     
    let g a =
      Textvariable.set txt ("début " ^ a);
      f a;
      Textvariable.set txt ("fin " ^ a);
    ;;
     
    let test_threads () = 
      ignore (Thread.create f "fils");
      f "parent";
      ignore (Thread.create g "fils");
      g "parent"
    ;;
     
    let b = Button.create ~text:" go " ~command:test_threads fen ;;
    let lab = Label.create ~textvariable:txt fen ;;
    Tk.pack [b] ;;
    Tk.pack [lab] ;;
     
    Tk.mainLoop () ;;
    La fonction f prend est lourde à exécuter (elle prend plus de temps à s'exécuter dans le parent que dans le fils, sans quoi le thread parent fini parfois avant le fils). Elle n'interagit pas avec Tk. La fonction g est identique mais en plus d'écrire sur la sortie standard, elle écrit dans la fenêtre.

    Quand le code s'exécute en l'état, ne plante pas mais seul 'fin parent' est affiché dans la fenêtre. Je suppose que c'est parce que la fenêtre n'a pas été mise à jour avant la fin des calculs. Du coup, je rajoute un 'Tk.update_idletasks ()' après 'Textvariable.set txt "truc"'. Segfault à l'execution !

    Du coup je suis un peu perdu.

    - Soit il n'est effectivement pas possible, avec Tk, d'avoir une fenêtre active dans deux threads, auquel cas ce que je comptais faire ne peux pas marcher.
    Dans ce cas là, je pourrais peut-être lancer mes calculs après avoir fait un double fork de manière à ne pas attendre dans le processus parent, et dire au processus fils d'envoyer un signal disant qu'il a terminé et qu'il faut que le processus parent lise le résultat depuis le pipe et s'update lui-même

    - Soit c'est possible et je m'y prends mal.

  3. #3
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut
    Salut,

    J'essaie toujours de résoudre mon problème.

    J'ai décidé de changer d'approche. Du coup, ma nouvelle idée c'est de :

    1. Lancer l'algo "lourd" dans un processus pour les raisons citées plus haut
    2. Utiliser un double fork pour éviter que le processus principal n'attende la fin de l'algo
    3. Utiliser un signal pour signaler processus principal que l'algo est terminé et qu'il peut lire le résultat depuis le pipe.


    J'ai essayé d'implémenter ça :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    type effector = unit -> unit
     
    let pending_res : effector Queue.t = Queue.create ()
     
    let add_to_pending f in_chan =
      let eff : effector = fun () ->
        let res = Marshal.from_channel in_chan in (* PROBLEME ICI *)
        close_in in_chan;
        f res
      in
      Queue.push eff pending_res 
     
    let process_res signmb =
      let eff = Queue.pop pending_res in
      eff ()
    ;;
     
    Sys.set_signal Sys.sigalrm (Sys.Signal_handle process_res) ;;
     
    let main_pid = Unix.getpid () ;;
     
    let launch algo arg f =
      let (fd_in, fd_out) = Unix.pipe () in
      match Unix.fork () with
      | 0 -> if Unix.fork () <> 0 then exit 0;
             Unix.close fd_in;
             let res = algo arg in
             let out_chan = Unix.out_channel_of_descr fd_out in
             Marshal.to_channel out_chan res [];
             Unix.kill main_pid Sys.sigalrm;
             close_out out_chan;
             exit 0
      | p -> ignore (Unix.wait ());
             Unix.close fd_out;
             let in_chan = Unix.in_channel_of_descr fd_in in
             add_to_pending f in_chan
    Ca compile mais je ne suis pas sûr que ça marche vraiment.

    un effector est une fonction ayant pour but d'actualiser l'environnement (selon les instructions de la fonction f et de son argument, le résultat de l'algo). Du coup, quand on veut langer un algo en background, on précise aussi l'effet que son résultat aura sur l'environnement. A partir de là, un effector est construit et ajouté à une queue. La queue est "lue" dès qu'on reçoit le signal qu'un processus a terminé.

    J'ai deux problèmes (au moins) :

    1. Est-ce que in_chan ne va pas être lue dès la definition de l'effecteur ? Sinon, comment faire pour éviter ça ?
    2. Comment faire pour préciser le type de Marshal.from_channel ? Dans l'idéal, je voudrais le passer comme argument à launch (comme f), de manière à qu'il soit intégré à l'effector par add_to_pending.


    Sinon, entre temps je suis tombé sur ça :

    http://ocamlnews.blogspot.fr/2012/05...-in-ocaml.html

    Je suis preneur de liens dans le genre soit d'explications un peu plus précises sur ce code (j'ai vu qu'il y avait eu un article écrit là dessus, mais il n'est pas en accès libre...)

    Merci d'avance !

  4. #4
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut
    J'essaie toujours de faire marcher mon code.

    Je suis parti dans une nouvelle direction. Je ne savais pas que Marshal.from_channel attendait qu'on écrive sur le pipe (je pensais que s'il n'y avait rien d'écrit ça renvoyait une erreur). Du coup, j'ai pensé à :

    1. (Toujours lancer l'algo dans un processus (avec double fork, pour ne pas l'attendre))
    2. Dans le processus principal, créer une référence "None". Puis créer un nouveau thread qui Marshal.from_channel le résultat depuis le pipe.
    3. Dans le thread principal, où tourne Tk, faire une fonction de polling non bloquante qui appelle une fonction dès que la référence n'est plus "None". Normalement, la fonction after de Tk devrait me permettre de faire ça.


    Mon implémentation :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let rec check_if_finished res eff () =
      match !res with
      | None -> Tk.after(100, check_if_finished res eff)
      | Some ans -> eff ans
     
    let launch alg args eff =
      let (fd_in, fd_out) = Unix.pipe () in
      match Unix.fork () with
      | 0 -> if Unix.fork () <> 0 then exit 0;
             Unix.close fd_in;
             let out_chan = Unix.out_channel_of_descr fd_out in
             let res = alg args in
             Marshal.to_channel out_chan res;
             close_out out_chan;
             exit 0
      | p -> ignore (Unix.wait ());
             Unix.close fd_out;
             let in_chan = Unix.in_channel_of_descr fd_in in
             let res = ref None in
             ignore (Thread.create
                       (fun ic -> res := Marshal.from_channel ic) in_chan);
             check_if_finished res eff ()
    Seulement, j'ai un problème majeur : c'est que la fonction after de Tk (enfin, de Tcl mais elle est tout le temps utilisée en Tk...) n'a pas l'air d'avoir été implémentée dans labltk ! (source : https://groups.google.com/forum/#!to...ml/aZmGle9k-ms - mais bon, ça date de 2005)

    Sans cette fonction, mon truc ne peux pas marcher. Je trouve vraiment ça bizarre qu'elle n'ait pas été implémentée en Labltk (on la rencontre dans la plupart des tuto tkinter par exemple. Les bindings Tk de Perl, Ruby, Python l'incluent...)

    Bon puis à part ça (mais c'est juste histoire de ronchonner), je trouve que le manque de documentation et de tutoriels de Labltk est vraiment un problème...

    Enfin, il me restera encore à trouver un moyen de préciser le type de ce que je Marshal.from_channel.

    PS : j'ai aussi vu que OCaml comme Tk fournissaient des interfaces simples pour travailler avec des sockets. Ca serait peut-être une meilleure façon de procéder ?..

    PPS : Si Lablgtk est mieux documenté que Labltk, je suis tout à fait prêt à changer (même si ça m'embête un peu vu que j'ai l'habitude de Tkinter...)

  5. #5
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut
    Bon je pense que je n'aurais plus de réponse après autant de temps

    Sinon, au cas où des gens taperaient quelque chose comme "mais où est la fonction after dans Labltk ?" dans Google et tomberaient sur cette page :

    A priori, la fonction after n'est pas implémentée en Labltk, la raison étant que "c'est du tcl et pas du tk". Je trouve ça un peu bizarre car cette fonction est quand même pas mal utilisée, mais bref.

    Par contre, il est possible d'implémenter le binding soi-même assez facilement. Voici une implémentation tirée de Camltk (je ne retrouve plus le lien, désolé. Mais en googlant un peu ça doit se trouver)

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let after_idle f =
      let id = Protocol.new_function_id () in
      let wrapped _ =
        Protocol.clear_callback id; (* do it first in case f raises exception *)
        f ()
      in
      Hashtbl.add Protocol.callback_naming_table id wrapped;
      ignore ( Protocol.tkEval
                [| Protocol.TkToken "after"; Protocol.TkToken "idle"; 
                   Protocol.TkToken ("camlcb " ^ Protocol.string_of_cbid id) |] )
    Je ne l'ai pas testée mais ça devrait marcher car je n'ai fait que rajouter les noms des modules pour que ça compile (mais vu que le reste de mon code ne marche pas...)

    Je suppose que pour after il suffit de remplacer "Protocol.TkToken "idle" par une chaîne de caractères représentant le nombre de millisecondes.

  6. #6
    Membre à l'essai
    Homme Profil pro
    Étudiant
    Inscrit en
    Juillet 2012
    Messages
    7
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2012
    Messages : 7
    Points : 10
    Points
    10
    Par défaut
    Bonjour,

    Je ne suis pas expert en programmation parallèle, mais je trouve ton idée
    d'utiliser 'fork' assez surprenante. Tu le dis toi-même : duplication du processus,
    et envoi des données.

    Je pense que les threads sont une meilleure option. Une manière
    d'appréhender ton problème est de penser au MCV (Model View Controller).

    Ton controller lance les algorithmes qui vont bien en fonction des événements
    (clics souris, touches de clavier, etc.). Ces algorithmes changent le modèle,
    et c'est la vue qui se change automatiquement en fonction de ton modèle.

    Au final, ça devrait te faire 3+N threads, N étant le nombre d'algorithmes
    étant en train de tourner.

    J'espère que ça peut t'aider.

    Cordialement,

  7. #7
    Membre régulier
    Étudiant
    Inscrit en
    Juillet 2010
    Messages
    102
    Détails du profil
    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Juillet 2010
    Messages : 102
    Points : 110
    Points
    110
    Par défaut
    Salut !

    Merci pour ta réponse (et désolé de ne pas y avoir répondu plus tôt - à vrai dire je ne m'attendais plus à avoir une réponse).

    La raison pour laquelle je voulais utiliser des processus plutôt que des threads est que je voulais pouvoir exploiter plusieurs processeurs. Dans mon cas, l'environnement devrait être relativement léger et les calculs sont lancés par l'utilisateur, ce qui fait qu'il ne devrait pas y en avoir un nombre énorme en même temps. C'est ce qui m'avait fait penser que les processus pourraient être une bonne option.

    Néanmoins, ce lien me fait penser que le support des multicores en OCaml n'est plus très loin, donc je vais me renseigner sur le model view controller.

    En tous cas encore merci pour ta réponse !

Discussions similaires

  1. Question sur la bonne façon de faire un JMenuBar
    Par bit_o dans le forum AWT/Swing
    Réponses: 2
    Dernier message: 26/06/2007, 11h26
  2. [Checkbox] La bonne façon et comment faire
    Par Meewix dans le forum Langage
    Réponses: 9
    Dernier message: 19/10/2006, 10h23

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo