Précédent   Forum du club des développeurs et IT Pro > Autres langages > Python & Zope > Général Python
Général Python Forum d'entraide sur les fondamentaux du langage Python, syntaxe, POO, bibliothèque standard, ...
Partagez cette discussion sur d'autres réseaux sociaux : Viadeo Twitter Google Facebook Digg Delicious MySpace Yahoo
Réponse
 
Outils de la discussion
Publicité
'
Vieux 16/01/2013, 00h28   #1
Kaarbok
Invité de passage
 
Homme
Inscription : 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 :

Citation:
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 ?
Kaarbok est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 16/01/2013, 08h09   #2
tyrtamos
Expert Confirmé
 
Avatar de tyrtamos
 
Inscription : décembre 2007
Messages : 1 798
Détails du profil
Informations personnelles :
Localisation : France

Informations forums :
Inscription : décembre 2007
Messages : 1 798
Points : 3 110
Points : 3 110
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
tyrtamos est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 16/01/2013, 11h02   #3
wiztricks
Expert Confirmé Sénior
 
Inscription : juin 2008
Messages : 3 739
Détails du profil
Informations forums :
Inscription : juin 2008
Messages : 3 739
Points : 4 581
Points : 4 581
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
wiztricks est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 16/01/2013, 17h50   #4
VV33D
Membre expérimenté
 
Inscription : août 2010
Messages : 516
Détails du profil
Informations forums :
Inscription : août 2010
Messages : 516
Points : 522
Points : 522
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.
VV33D est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 16/01/2013, 23h29   #5
Kaarbok
Invité de passage
 
Homme
Inscription : 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
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 ?
Kaarbok est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 17/01/2013, 15h19   #6
wiztricks
Expert Confirmé Sénior
 
Inscription : juin 2008
Messages : 3 739
Détails du profil
Informations forums :
Inscription : juin 2008
Messages : 3 739
Points : 4 581
Points : 4 581
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
wiztricks est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 17/01/2013, 15h34   #7
wiztricks
Expert Confirmé Sénior
 
Inscription : juin 2008
Messages : 3 739
Détails du profil
Informations forums :
Inscription : juin 2008
Messages : 3 739
Points : 4 581
Points : 4 581
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
wiztricks est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 21/01/2013, 19h57   #8
Kaarbok
Invité de passage
 
Homme
Inscription : 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
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 )
Kaarbok est déconnecté   Envoyer un message privé Réponse avec citation 01
Vieux 22/01/2013, 19h59   #9
wiztricks
Expert Confirmé Sénior
 
Inscription : juin 2008
Messages : 3 739
Détails du profil
Informations forums :
Inscription : juin 2008
Messages : 3 739
Points : 4 581
Points : 4 581
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()

Citation:
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
wiztricks est déconnecté   Envoyer un message privé Réponse avec citation 00
Réponse Cette discussion est résolue.
Outils de la discussion

Navigation rapide


Fuseau horaire GMT +2. Il est actuellement 20h25.


 
 
 
 
Partenaires

Hébergement Web