Publicité
+ Répondre à la discussion
Affichage des résultats 1 à 9 sur 9
  1. #1
    Invité de passage
    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 : 0
    Points
    0

    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 :
    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 Confirmé
    Avatar de tyrtamos
    Profil pro
    Inscrit en
    décembre 2007
    Messages
    2 185
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : décembre 2007
    Messages : 2 185
    Points : 3 724
    Points
    3 724

    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.
    Ne rien ranger permet d'observer la loi universelle d'entropie: l'inévitable convergence vers le chaos...
    Mes recettes python: http://www.jpvweb.com

  3. #3
    Expert Confirmé Sénior
    Homme Profil pro
    Architecte technique
    Inscrit en
    juin 2008
    Messages
    4 748
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Architecte technique
    Secteur : Industrie

    Informations forums :
    Inscription : juin 2008
    Messages : 4 748
    Points : 7 160
    Points
    7 160

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

  4. #4
    Membre émérite
    Inscrit en
    août 2010
    Messages
    822
    Détails du profil
    Informations forums :
    Inscription : août 2010
    Messages : 822
    Points : 846
    Points
    846

    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
    Invité de passage
    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 : 0
    Points
    0

    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 Confirmé Sénior
    Homme Profil pro
    Architecte technique
    Inscrit en
    juin 2008
    Messages
    4 748
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Architecte technique
    Secteur : Industrie

    Informations forums :
    Inscription : juin 2008
    Messages : 4 748
    Points : 7 160
    Points
    7 160

    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 :
    canvas.bind("<Button-1>", lambda e: create_ball(e.widget))
    à la place de:
    Code :
    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 :
    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 :
    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

  7. #7
    Expert Confirmé Sénior
    Homme Profil pro
    Architecte technique
    Inscrit en
    juin 2008
    Messages
    4 748
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Architecte technique
    Secteur : Industrie

    Informations forums :
    Inscription : juin 2008
    Messages : 4 748
    Points : 7 160
    Points
    7 160

    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

  8. #8
    Invité de passage
    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 : 0
    Points
    0

    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 Confirmé Sénior
    Homme Profil pro
    Architecte technique
    Inscrit en
    juin 2008
    Messages
    4 748
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Architecte technique
    Secteur : Industrie

    Informations forums :
    Inscription : juin 2008
    Messages : 4 748
    Points : 7 160
    Points
    7 160

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

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

Liens sociaux

Règles de messages

  • Vous ne pouvez pas créer de nouvelles discussions
  • Vous ne pouvez pas envoyer des réponses
  • Vous ne pouvez pas envoyer des pièces jointes
  • Vous ne pouvez pas modifier vos messages
  •