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

Python Discussion :

Difficultés avec le Threading


Sujet :

Python

  1. #1
    Nouveau Candidat au Club
    Homme Profil pro
    Inscrit en
    Janvier 2013
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Janvier 2013
    Messages : 3
    Points : 1
    Points
    1
    Par défaut Difficultés avec le Threading
    Bonsoir,
    comme dit dans le titre, je rencontre quelques problèmes avec mon code. Je l'ai réduit au maximum pour ne garder que ce qui coince :

    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
    from tkinter import *
    import time, threading
     
    class mon_canvas (Canvas) :
        def __init__ (self, parent) :
            Canvas.__init__(self, parent, width = 600, height = 600, bg  = "orange")
            self.bind("<Button-1>",self.clic_gauche)
            self.pack()
        def clic_gauche(self, event) :
            self.nouveau_circle = self.create_oval(100, 100, 140, 140, fill="blue", outline="blue")
            self.nouveau_circle = Cercle(self, self.nouveau_circle)
            self.nouveau_circle.start()
     
    class Cercle (threading.Thread) :
        def __init__ (self, parent, cercle_id) :
            threading.Thread.__init__(self)
            self.parent = parent
            self.id = cercle_id
     
        def run (self) :
            time.sleep(.01)
            x = 100
            while x<500 :
                x += 1
                self.parent.coords(self.id, x, x, x+40, x+40)
                time.sleep(.01)
     
    root = Tk()
    mon_canvas(root).mainloop()
    Le programme affiche - comme je le souhaite - un Canvas qui fait apparaître des cercles mouvants quand on clique dessus. Cependant, dans ~10% des cas (et plus particulièrement quand on spam-clic j'ai l'impression), un message d'erreur survient dans le shell :

    Exception in thread Thread-2: (NB : L'erreur ne concerne pas forcément ce thread)
    Traceback (most recent call last):
    File "C:...\Python\lib\threading.py", line 639, in _bootstrap_inner
    self.run()
    File "C:.../tests multithreading.py", line 25, in run
    self.parent.coords(self.id, x, x, x+40, x+40)
    File "C:...\Python\lib\tkinter\__init__.py", line 2264, in coords
    self.tk.call((self._w, 'coords') + args))]
    File "C:...\Python\lib\tkinter\__init__.py", line 2262, in <listcomp>
    return [getdouble(x) for x in
    ValueError: could not convert string to float: 'expected'
    Le Thread est stoppé, et donc le cercle concerné s'immobilise.
    Je ne pense pas que c'est lié à Tkinter, mais plutôt à une mauvaise utilisation du Threading de ma part. Des idées d'où cela peut venir ?

  2. #2
    Expert éminent
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 461
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 461
    Points : 9 248
    Points
    9 248
    Billets dans le blog
    6
    Par défaut
    Bonjour,

    Il y a longtemps que je n'ai pas travaillé avec tkinter, mais je crois qu'un thread ne doit pas toucher au graphique. Sur la bibliothèque graphique que j'utilise (PyQt4), le thread envoie un message au programme principal qui, lui, agit sur le graphique.
    Un expert est une personne qui a fait toutes les erreurs qui peuvent être faites, dans un domaine étroit... (Niels Bohr)
    Mes recettes python: http://www.jpvweb.com

  3. #3
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 283
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 283
    Points : 36 770
    Points
    36 770
    Par défaut
    Salut,

    tkinter supporte le threading en sérialisant les appels à la librairie TCL/Tk via un verrou. Là ou çà coince, c'est côté "objets" Python, i.e. sérialiser les références pour transformer l'appel à "self.parent.coords(self.id, x, x, x+40, x+40)" jusqu'au verrouillage.
    Pour faire "marcher" le code, il faut limiter les dépendances sur les références.
    Exemple:
    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
    import tkinter as tk
    import time
    from threading import Thread
     
    def ball_move(canvas, iid):
        dx = 1
        x = 100
        while x < 500:
            canvas.move(iid, dx, dx)
            x += dx
            time.sleep(.01)
     
    def create_ball(canvas):
     
        iid = canvas.create_oval(100, 100, 140, 140, fill="blue", outline="blue")
        th = Thread(target=ball_move, args=(canvas, iid))
        th.daemon = True
        th.start()
     
    def on_left_click(e):
        w = e.widget
        create_ball(w)
     
    app = tk.Tk()
    canvas = tk.Canvas(width = 600, height = 600, bg  = "orange")
    canvas.pack()
    canvas.bind("<Button-1>",on_left_click)
    tk.mainloop()
    Mais si cela vaut comme "exemple" sur le pourquoi çà coince, pas facile de coder avec ce genre de restrictions.

    De toutes façons, les appels à TCL/Tk sont sérialisés, on ne gagne pas grand chose à les distribuer sur plusieurs threads. De plus, Python à son propre mécanisme de sérialisation (GIL): un ou plusieurs threads consommeront difficilement plus que la capacité d'un seul CPU.

    La solution (basique) sera de réaliser:
    - le calcul de la position suivante,
    - le déplacement des objets,
    dans le même thread et de séquencer ces activités via l'event loop de TCL:

    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
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    import tkinter as tk
    import time
    from threading import Thread
     
    def center(canvas, ident):
        c = canvas.bbox(ident)
        if c:
            x0, y0, x1, y1 = c
            return (x1 - x0) // 2, (y1 - y0) // 2
     
    class Ball:
        iid = None
        cx = cy = None
        canvas = None
     
        def __init__(self, canvas):
            self.iid = canvas.create_oval(100, 100, 140, 140, fill="blue", outline="blue")
            self.cx, self.cy = center(canvas, self.iid)
            self.canvas = canvas
     
        def move(self, dx=1, dy=1):
            if self.cx + dx < 500:
                self.canvas.move(self.iid, dx, dy)
                self.cx += dx
     
    balls = []
    def create_ball(canvas):
        b = Ball(canvas)
        balls.append(b)
     
    def on_left_click(e):
        w = e.widget
        create_ball(w)
     
    timer = None
    def do_update():
        global timer
        for b in balls:
            b.move()
        timer = app.after(20, do_update)
     
     
    app = tk.Tk()
    canvas = tk.Canvas(width = 600, height = 600, bg  = "orange")
    canvas.pack()
    canvas.bind("<Button-1>",on_left_click)
    do_update()
    tk.mainloop()
    C'est un modèle qui en gros actualise la frame affichée tous les 20ms, ce qui correspond à 50 fps. Tant que le calcul de la position des objets dans la frame "à venir" tient dans ces 20ms, pas besoin de "plus de CPU".
    Si le nombre d'objets devient important et qu'on s'amuse à calculer des collisions, il faudra peut être pouvoir répartir la charge sur plusieurs CPU.
    Cela ne va pas trop changer ce design, jusque compliquer l'initialisation de l'application et l'interface entre do_update et la récupération de la position des objets dans la frame suivante.
    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  4. #4
    Membre éprouvé
    Inscrit en
    Août 2010
    Messages
    1 124
    Détails du profil
    Informations forums :
    Inscription : Août 2010
    Messages : 1 124
    Points : 1 277
    Points
    1 277
    Par défaut
    Il est possible d'alterner le code Tkinter et votre code Python sur le thread principal à l'aide des générateurs, car la mainloop() de tkinter peut etre limitée en durée. Twisted peut surement aider pour cette approche.

    Je l'ai fait moi même, from scratch, et le seul patch nécessaire concerne les appels périodique de tkinter.

    J'ai également croisé une Python recipe qui permettait de faire tourner Tkinter sur un thread non principal.

  5. #5
    Nouveau Candidat au Club
    Homme Profil pro
    Inscrit en
    Janvier 2013
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Janvier 2013
    Messages : 3
    Points : 1
    Points
    1
    Par défaut
    Bonsoir à tous,
    tout d'abord, merci pour vos réponses
    Je pensais avoir à peu près compris la cause du problème, mais je viens de me rendre compte qu'en remplaçant mon coords(...) par un move(...), le programme fonctionne correctement !
    Je ne comprends du tout pourquoi...

    wiztricks, les deux codes que tu as proposé m'ont appris pas mal de choses, mais quelque chose m'intrigues dans le deuxième : tu bind le clic gauche sur une fonction on_left_click qui va instantanément appeler une deuxième fonction : create_ball. Pourquoi ne pas directement bind le clic gauche sur create_ball ?

  6. #6
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 283
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 283
    Points : 36 770
    Points
    36 770
    Par défaut
    Salut,

    Citation Envoyé par Kaarbok Voir le message
    wiztricks, les deux codes que tu as proposé m'ont appris pas mal de choses, mais quelque chose m'intrigues dans le deuxième : tu bind le clic gauche sur une fonction on_left_click qui va instantanément appeler une deuxième fonction : create_ball. Pourquoi ne pas directement bind le clic gauche sur create_ball ?
    En voilà une question!
    C'est du code écrit à la volée. Par réflexe, je fais des "bind" sur des on_... et des "commands" sur des do_...,
    J'ai une préférence pour l'auto-documentation et la production de code où les fonctions réalisent peu de modifs. Le but étant de pouvoir construire autrement sans tout avoir à remettre à plat.
    Est ce qu'on optimise quoi que ce soit "en faisant bind de clic gauche sur create_ball"? En fait, create_ball n'attendant pas d'event en paramètre, on pourrait écrire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    canvas.bind("<Button-1>", lambda e: create_ball(e.widget))
    à la place de:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    canvas.bind("<Button-1>", on_left_click)
    On a supprimé le callback "on_left_click" pour le remplacer par un lambda.
    Moins de lignes dans le script, n'optimise pas grand chose surtout si on change d'avis et accrocher plus d'action à cet événement. Pour optimiser "vraiement", on pourrait écrire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
     
    canvas.bind("<Button-1>", self.register(canvas.create_ball, ''))
    En enregistrant directement le "callback", on évite d'avoir à récupérer les 36 paramètres d'event. Mais c'est une pratique peu courante.
    Note: pousser .create_ball dans une instance de canvas permet de ne pas expliquer la syntaxe:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    canvas.bind("<Button-1>", self.register(create_ball, '%W'))
    et les modalités de récupération du widget tkinter à partir de là (i.e. le nom du script Tk).

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  7. #7
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 283
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 283
    Points : 36 770
    Points
    36 770
    Par défaut
    Salut,

    Citation Envoyé par Kaarbok Voir le message
    Je pensais avoir à peu près compris la cause du problème, mais je viens de me rendre compte qu'en remplaçant mon coords(...) par un move(...), le programme fonctionne correctement !
    Je ne comprends du tout pourquoi...
    Lisez les sources de tkinter.
    Vous verrez que les dépendances de .coords avec ce qui se passe côté Python sont bien plus importantes que celle d'un .move.(*)

    Dans le cas threads, on a seulement réduit la probabilité de se vautrer.
    Ce "mieux" est insuffisant pour construire un code robuste.

    - W
    (*) soit on dit:
    - çà devrait marcher "pareil":
    Dans ce cas + de dépendances = plus probable de passer dans des branches de code buggé,
    - çà ne peut pas marcher:
    Et + de dépendances = plus de chance de le "montrer"
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  8. #8
    Nouveau Candidat au Club
    Homme Profil pro
    Inscrit en
    Janvier 2013
    Messages
    3
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations forums :
    Inscription : Janvier 2013
    Messages : 3
    Points : 1
    Points
    1
    Par défaut
    Salut,
    je pense aussi que pour des programmes plus complexes, il vaut mieux décomposer au maximum les fonctions.
    Une dernière question qui m'a traversé l'esprit en voyant ta fonction do_update() : dans des programmes plus importants, c'est un concept à éviter ? Parce que si (par exemple) je veux que chaque rond se déplace dans une direction différente, il faudrait passer l'argument dans toutes les fonctions intermédiaire. Et puis, j'imagine qu'au bout d'un moment, la fonction update ressemblerait à un fourre-tout de tous les objets à actualiser...
    Donc, ne vaut-il pas mieux gérer chaque objet avec un thread "autonome" ?
    (au passage, désolé pour le temps de réponse plutôt long, j'avais pas mal de boulot ces dernier temps )

  9. #9
    Expert éminent sénior
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 283
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 283
    Points : 36 770
    Points
    36 770
    Par défaut
    Citation Envoyé par Kaarbok Voir le message
    Une dernière question qui m'a traversé l'esprit en voyant ta fonction do_update() : dans des programmes plus importants, c'est un concept à éviter ? Parce que si (par exemple) je veux que chaque rond se déplace dans une direction différente, il faudrait passer l'argument dans toutes les fonctions intermédiaire.
    Le déplacement d'un objet, passe par fonction "next_position" qui calculera la position "suivante" en fonction de:
    • la position courante,
    • l'unité de temps (dt).

    Le dx, dy à envoyer à .move sera cette différence.
    Dans l'exemple, le calcul de .next_position et le .move sont imbriqués.
    Si on complique, il faudra séparer/décomposer.
    Le "dt" est lié à l'intervalle de temps des do_update.
    Ca donne un code du genre:
    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
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    import tkinter as tk
    import time
     
    def center(canvas, ident):
        c = canvas.bbox(ident)
        if c:
            x0, y0, x1, y1 = c
            return (x1 - x0) // 2, (y1 - y0) // 2
     
    class Ball:
        iid = None
        cx, cy = None
     
        def __init__(self):
            self.iid = canvas.create_oval(100, 100, 140, 140, fill="blue", outline="blue")
            self.cx, self.cy = center(canvas, self.iid)
     
        def move(self, dx, dy):
            canvas.move(self.iid, dx, dy)
            self.cx += dx
            self.cy += dy
     
        @property
        def next_position(self):
            if self.cx + 1 < 500:
                return self.cx+1, self.cy+1
     
     
    balls = []
    def create_ball():
        b = Ball()
        balls.append(b)
     
    def on_left_click(e):
        w = e.widget
        create_ball()
     
    timer = None
    def do_update(delay=20):
        global timer
     
        now = time.clock()
     
        for b in balls:
            x0, y0 = b.cx, b.cy
            x1, y1 = b.next_position
            b.move(x1-x0, y1-y0)
     
        rest = int(delay - (1000 * (time.clock() - now)))
        assert rest > 2, 'fatal: rest=%.3f' % rest
        timer = app.after(rest, do_update)
     
     
    app = tk.Tk()
    canvas = tk.Canvas(width = 600, height = 600, bg  = "orange")
    canvas.pack()
    canvas.bind("<Button-1>",on_left_click)
    do_update()
    tk.mainloop()

    Donc, ne vaut-il pas mieux gérer chaque objet avec un thread "autonome" ?
    En quoi ce serait "mieux"?
    Si la mesure est le coût CPU par balles, associer un thread à chaque balle sera moins bon que de réduire cela à:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
        for b in balls:
            x0, y0 = b.cx, b.cy
            x1, y1 = b.next_position
            b.move(x1-x0, y1-y0)
    Si on ajoute la gestion des collisions, le déplacement de chaque balle ne sera plus indépendant de celui des autres: il faudra séparer le calcul de la position suivante de l'ensemble des balles et celui de l'affichage.

    Ce qui fait pousser une fonction, genre:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    def compute_next():
        for b in balls:
            x, y = b.cx, b.cy
            dx, dy = b.dxy
            if x + dx < 500:
                b.next_position = x+dx, y+dx
            else:
                b.next_position = x, y
    qui calcule la position suivante de toutes les balles.
    Normalement, en fonction des collisions, mais ce n'est pas le sujet.

    La quantité de calculs dépend du nombre de balles et d'un nombre de collisions qui varie à chaque itération.
    Je ne sais pas combien de balles, on pourrait traiter de cette façon.

    Pour en gérer "plus", il pourra être intéressant de traiter la partie calcul de façon asynchrone pour calculer N coups d'avance et "lisser la charge".

    Ceci dit, le threading ne permettra pas d'utiliser les autres CPU du système: pour cela il faut pousser compute_next dans un autre process via multiprocessing et le mettre en pause lorsqu'il aura N itérations d'avance sur l'affichage.

    Mais, le code à réaliser pour synchroniser ces deux taches sera plus lourd et beaucoup plus difficile à mettre au point.

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Difficulté avec un thread
    Par burndev dans le forum SWT/JFace
    Réponses: 2
    Dernier message: 08/06/2012, 11h06
  2. Des problemes avec ces threads <pthread.h>
    Par nasamad dans le forum GTK+ avec C & C++
    Réponses: 26
    Dernier message: 07/07/2006, 12h46
  3. Réponses: 5
    Dernier message: 10/05/2005, 10h22
  4. [langage] Perl a t'il été compiler avec les threads
    Par vodevil dans le forum Langage
    Réponses: 2
    Dernier message: 07/05/2005, 15h00
  5. Difficultés avec TMenuItem.OnDrawItem
    Par ybruant dans le forum Composants VCL
    Réponses: 4
    Dernier message: 12/01/2005, 11h07

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