Forum Programmation.python PyGTK et les threads.

Posté par  .
Étiquettes : aucune
0
1
sept.
2010

Bonjour. J'essaye de faire la synthèse de ce que j'ai glané sur la toile à propos de pygtk et les threads. J'essaye donc un programme très simple d'expérimentation. J'ai une classe principale, une qui gère 2 threads et une classe thread proprement dite.

Mon problème : lorsque les threads 1 et 2 sont lancés simultanément, j'ai toujours un seul des deux labels qui est mis à jour (analogie avec une situation XOR).

Je commence à bloquer, je n'ai pas trouvé de documentation vraiment approfondie sur ce sujet. D'avance merci.


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from threads_manager import ThreadsManager
import gobject
gobject.threads_init() # <-----
import gtk

class Main:

def __init__(self):
self.window = gtk.Window()
self.button1 = gtk.Button('Start/stop thread 1')
self.button2 = gtk.Button('Start/stop thread 2')
self.labels = []
self.labels.append(gtk.Label('*'))
self.labels.append(gtk.Label('*'))
self.table = gtk.Table()
self.table.attach(self.labels[0], 0, 1, 0, 1, xpadding=10)
self.table.attach(self.button1, 1, 2, 0, 1)
self.table.attach(self.labels[1], 0, 1, 1, 2, xpadding=10)
self.table.attach(self.button2, 1, 2, 1, 2)
self.window.add(self.table)
self.threads_manager = ThreadsManager(self)
self.button1.connect('clicked', self.threads_manager.switch_thread_state, 1)
self.button2.connect('clicked', self.threads_manager.switch_thread_state, 2)
self.window.connect('destroy', self.quit)
self.window.set_position(gtk.WIN_POS_CENTER)
self.window.show_all()

def main(self):
gtk.main()
return 0

def quit(self, widget_window):
# À finir. Informer les threads qu'ils doivent stopper afin de
# quitter proprement ?
gtk.main_quit()

if __name__ == '__main__':
main = Main()
main.main()


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading
from my_thread import MyThread

class ThreadsManager:

def __init__(self, main):
self.main = main
self.th1 = MyThread(self.main, 1)
self.th2 = MyThread(self.main, 2)

def switch_thread_state(self, widget_button, nb):
if nb == 1:
if self.th1.is_running == True:
self.th1.stop()
else:
self.th1.start()
else: # Therefore nb == 2
if self.th2.is_running == True:
self.th2.stop()
else:
self.th2.start()


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import threading, time
import gobject
import gtk

class MyThread(threading.Thread):

def __init__(self, main, nb):
threading.Thread.__init__(self)
self.main = main
self.nb = nb
self.is_running = False

def start(self):
print 'start thread', self.nb
self.is_running = True
while self.is_running == True:
gobject.idle_add(self.update_label) # <-----
time.sleep(0.1)
gtk.main_iteration() # <-----

def stop(self):
print 'stop thread', self.nb
self.is_running = False

def update_label(self):
# Le but de cette fonction est de montrer comment modifier
# un objet gtk dans la boucle principale depuis un thread.
self.main.labels[self.nb-1].set_text(str(time.time()))

  • # Une solution

    Posté par  (site web personnel) . Évalué à 1.

    j'ai réécrit la classe MyThread comme ceci:
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    import threading, time
    import gobject
    import gtk
    
    class MyThread:
        def __init__(self, main, nb):
            self.main = main
            self.nb = nb
            self.is_running = False
    
        def start(self):
            print 'start thread', self.nb
            self.is_running = True
            self.th = threading.Thread(target = self.loop)
            self.th.start()
    
        def stop(self):
            print 'stop thread', self.nb
            self.is_running = False
            self.th = None
    
        def loop(self):
            while self.is_running == True:
                gobject.idle_add(self.update_label)
                time.sleep(0.1) 
    
        def update_label(self):
            # Le but de cette fonction est de montrer comment modifier
            # un objet gtk dans la boucle principale depuis un thread.
            self.main.labels[self.nb-1].set_text(str(time.time()))
    
    • [^] # Re: Une solution

      Posté par  (site web personnel) . Évalué à 1.

      Quelques notes, puisque j'y suis:
      - les tests "if x == True" et "while y == False" peuvent être réécrits "if x" et "while not y"
      - la classe MyThread pose des problèmes de synchronisation ("race conditions"): il se peut que la valeur de is_running soit fausse si un autre thread intervient par ex.
      - j'ai oublié "self.th.join()" avant "self.th = None", pour attendre que le thread ait terminé avant de revenir.
      • [^] # Re: Une solution

        Posté par  . Évalué à 1.

        Merci beaucoup ! Je commencais à tourner en rond.

        OK, j'avais hérité MyThread de la classe threading.Thread parce que tous les exemples procédaient ainsi. Bon, j'ai un exemple fonctionnel je vais compulser à fond la doc sur les threads. Merci encore.
    • [^] # Re: Une solution

      Posté par  . Évalué à 2.

      Effectivement, c'est un peu mieux, mais thor_tue, va lire la doc avant de te lancer là-dedans !!

      Tout d'abord, comme Achille l'a fait, pour une simple boucle, on ne sous-classe pas Thread. Ou alors, si tu veux le faire, lis la doc qui dit :

      There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding the run() method in a subclass. No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the __init__() and run() methods of this class.

      Dans l'exemple que tu donnes, thor_tue, tu n'as simplement jamais créé de threads ! Tout s'éxécutait dans le processus principal, d'où l'exclusivité mutuelle que tu observais …

      Cependant, la solution d'Achille n'est pas la bonne non plus : tu utilises bien des threads, mais ils ne servent qu'à attendre 0.1s et lancer la mise à jour des labels … dans le processus principal !

      La fonctions gobject.idle_add() sert à faire du « faux » multithread en exécutant une fonction dans le processus principal quand elle a le temps (entre deux mises à jours de widget). C'est pratique quand tu ne veux pas utiliser de threads, mais inutile dans notre cas.

      Ensuite, le gtk.main_iteration() c'est vraiment un hack : en général, quand tu l'utilises, c'est que tu as mal fait quelque chose. Ce n'est qu'à utiliser que dans des cas très particuliers.

      Les fonctions qui nous seront utiles sont gtk.gdk.threads_enter() et gtk.gdk.threads_leave(). Elles permettent à des threads en dehors du « principal », qui fait tourner la boucle d'évènements, d'aller tripoter la GUI sans interférer avec cette boucle, de manière thread-safe. En bref, elle va « juste » demander un lock à la boucle principale lui permettant de jouer avec la GUI jusqu'à ce qu'elle le relâche. On doit donc minimiser le temps passé entre en enter() et un leave(), car la boucle principale est bloquée pendant ce temps !

      En gros, je pense qu'il faut que vous saisissiez bien le principe d'une boucle d'évènements avant de vous lancer là-dedans. Désolé je n'ai pas de liens sous la main.

      Voici ma version :

      #!/usr/bin/env python
      # -*- coding: utf-8 -*-

      import threading, time
      import gobject
      import gtk
      import gtk.gdk

      class MyThread:
      def __init__(self, main, nb):
      self.main = main
      self.nb = nb
      self.is_running = False

      def start(self):
      print 'start thread', self.nb
      self.is_running = True
      self.th = threading.Thread(target = self.loop)
      self.th.start()

      def stop(self):
      print 'stop thread', self.nb
      self.is_running = False
      self.th.join()
      self.th = None

      def loop(self):
      while self.is_running == True:
      gtk.gdk.threads_enter()
      self.update_label()
      gtk.gdk.threads_leave()
      time.sleep(0.1)

      def update_label(self):
      # Le but de cette fonction est de montrer comment modifier
      # un objet gtk dans la boucle principale depuis un thread.
      self.main.labels[self.nb-1].set_text(str(time.time()))


      Enfin, comme dis Achille, tu peux toujours avoir un problème de race-condition sur ta variable is_running. Il vaut mieux utiliser des locks ou autre.
      Ah oui, au passage, la version d'Achille crée un thread à chaque start(). Si tu ne veux pas ce comportement, il faudra aussi jouer des lucks ou des variables de condition.
      • [^] # Re: Une solution

        Posté par  . Évalué à 2.

        Bien sûr, j'ai perdu l'indentation … Comment vous faites ?
        J'espère que c'est lisible quand même.
        • [^] # Re: Une solution

          Posté par  (site web personnel) . Évalué à 1.

          Je préfère malgré tout l'approche gobject.idle_add(). C'est une question de préférence personnelle, peut-être, mais la plupart des bibliothèques graphiques n'autorisent pas les appels cross-thread, même avec des verrous. D'ailleurs, que se passe-t-il si une exception est levée entre threads_enter() et threads_leave() ?
          Pour préserver les espaces dans le code, utilise des balises <pre>. Lis la doc ;-)
          • [^] # Re: Une solution

            Posté par  . Évalué à 2.

            Je préfère malgré tout l'approche gobject.idle_add().

            Pourquoi pas. Mais comme je pensais que ton but était de pouvoir faire des appels gtk depuis un autre thread, je ne voyais pas pourquoi tu l'utilises.

            Par contre, le idle_add() ne te dispense pas de bien locker les accès/écritures aux données de ton thread, car c'est le thread principal qui exécutera le code à ce moment-là.

            C'est une question de préférence personnelle, peut-être, mais la plupart des bibliothèques graphiques n'autorisent pas les appels cross-thread, même avec des verrous.

            Oui, effectivement, ce n'est pas forcément préconisé, mais c'est possible avec gtk.

            D'ailleurs, que se passe-t-il si une exception est levée entre threads_enter() et threads_leave() ?

            Tu mets ton leave() dans le finally:, comme indiqué dans le lien que tu donnes en dessous.

            Pour préserver les espaces dans le code, utilise des balises . Lis la doc ;-)

            Arf, bien vu … Merci ;-)
  • # La suite...

    Posté par  . Évalué à 1.

    1) Ok, je vois pourquoi ça ne créait pas de thread. (J'ai lu la doc., mais sans exemple fonctionnel c'est vraiment peu efficace pour assimiler !)

    2) Mon premier essai je l'ai fait avec la méthode leave/enter mais la FAQ semblait déconseiller cette méthode :
    [http://faq.pygtk.org/index.py?file=faq20.006.htp&req=sho(...)]

    Je manque évidemment de recul pour trancher, je mémorise vos deux méthodes pour l'instant.
    • [^] # Re: La suite...

      Posté par  . Évalué à 2.

      Mon premier essai je l'ai fait avec la méthode leave/enter mais la FAQ semblait déconseiller cette méthode

      C'est déconseillé _sous Windows_. Sous linux, pas de problème.

      Je manque évidemment de recul pour trancher, je mémorise vos deux méthodes pour l'instant.

      Ça va dépendre de ce que tu veux faire exactement, mais on n'avait pas beaucoup d'infos ici pour ton projet concret ! À toi d'évaluer ce qui te conviens le mieux.
    • [^] # Re: La suite...

      Posté par  . Évalué à 1.

      mais sans exemple fonctionnel c'est vraiment peu efficace pour assimiler

      même problème pour moi : je dois utiliser des Thread dans une appli pyGtk (linux/windows) et j'ai trouvé aucun exemple simple et concret, que j'arrive à comprendre et qui fonctionne.

      Je suis ouvert à tout bout de code montrant comment utiliser les Threads et qui soit facilement adaptable.
      • [^] # Re: La suite...

        Posté par  . Évalué à 2.

        Après 2 min de recherche, je suis tombé sur ce post, qui est en gros ma méthode plus la gestion correcte de l'accès à la variable partagée : http://aruiz.typepad.com/siliconisland/2006/04/threads_on_py(...)

        Mais bon, dire « je veux faire des threads » et ne pas trouver d'exemple, c'est normal : c'est tellement vague… Je vous conseillerais de d'abord apprendre la programmation threadée sans GUI, et après passer à PyGTK + threads. Et puis, être un peu plus précis sur ce que vous voulez faire.

        Par contre, si vous voulez juste copier/coller un bout de code sans réfléchir parce qu'on vous a demandé d'écrire un bout de code avec thread, c'est normal de rien trouver…

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.