Question

En tant que nouveau venu dans le développement d’une interface graphique en Python (avec pyGTK), je viens tout juste d’apprendre à utiliser le threading. Pour tester mes compétences, j'ai écrit une simple interface GTK avec un bouton Démarrer / Arrêter. L’objectif est que lorsqu’on clique dessus, un fil de discussion commence à incrémenter rapidement un nombre dans la zone de texte, tout en maintenant la réactivité de l’interface graphique.

J'ai bien l'interface graphique, mais j'ai des problèmes avec le threading. C'est probablement un problème simple, mais mon esprit est à propos de frit pour la journée. Ci-dessous, j'ai d'abord collé le rétrolien de l'interpréteur Python, suivi du code. Vous pouvez aller sur http://drop.io/pxgr5id pour le télécharger. J'utilise bzr pour le contrôle des révisions. Par conséquent, si vous souhaitez apporter une modification et la supprimer à nouveau, veuillez valider les modifications. Je colle également le code à l'adresse http://dpaste.com/113388/ , car il peut contenir des numéros de ligne. , et ce truc de démarque me donne un mal de tête.

Mise à jour 27 janvier, 15h52 HE: Le code légèrement mis à jour peut être trouvé ici: http://drop.io/threagui / asset / thread-gui-rev3-tar-gz

Traceback

crashsystems@crashsystems-laptop:~/Desktop/thread-gui$ python threadgui.pybtnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 39, in on_btnStartStop_clicked
    self.thread.stop()
  File "threadgui.py", line 20, in stop
    self.join()
  File "/usr/lib/python2.5/threading.py", line 583, in join
    raise RuntimeError("cannot join thread before it is started")
RuntimeError: cannot join thread before it is started
btnStartStop clicked
threadStop = 1
btnStartStop clicked
threadStop = 0
btnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 36, in on_btnStartStop_clicked
    self.thread.start()
  File "/usr/lib/python2.5/threading.py", line 434, in start
    raise RuntimeError("thread already started")
RuntimeError: thread already started
btnExit clicked
exit() called

Code

#!/usr/bin/bash
import gtk, threading

class ThreadLooper (threading.Thread):
    def __init__ (self, sleep_interval, function, args=[], kwargs={}):
        threading.Thread.__init__(self)
        self.sleep_interval = sleep_interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.finished = threading.Event()

    def stop (self):
        self.finished.set()
        self.join()

    def run (self):
        while not self.finished.isSet():
            self.finished.wait(self.sleep_interval)
            self.function(*self.args, **self.kwargs)

class ThreadGUI:
    # Define signals
    def on_btnStartStop_clicked(self, widget, data=None):
        print "btnStartStop clicked"
        if(self.threadStop == 0):
            self.threadStop = 1
            self.thread.start()
        else:
            self.threadStop = 0
            self.thread.stop()
        print "threadStop = " + str(self.threadStop)

    def on_btnMessageBox_clicked(self, widget, data=None):
        print "btnMessageBox clicked"
        self.lblMessage.set_text("This is a message!")
        self.msgBox.show()

    def on_btnExit_clicked(self, widget, data=None):
        print "btnExit clicked"
        self.exit()

    def on_btnOk_clicked(self, widget, data=None):
        print "btnOk clicked"
        self.msgBox.hide()

    def on_mainWindow_destroy(self, widget, data=None):
        print "mainWindow destroyed!"
        self.exit()

    def exit(self):
        print "exit() called"
        self.threadStop = 1
        gtk.main_quit()

    def threadLoop(self):
        # This will run in a thread
        self.txtThreadView.set_text(str(self.threadCount))
        print "hello world"
        self.threadCount += 1

    def __init__(self):
        # Connect to the xml GUI file
        builder = gtk.Builder()
        builder.add_from_file("threadgui.xml")

        # Connect to GUI widgets
        self.mainWindow = builder.get_object("mainWindow")

        self.txtThreadView = builder.get_object("txtThreadView")
        self.btnStartStop = builder.get_object("btnStartStop")
        self.msgBox = builder.get_object("msgBox")
        self.btnMessageBox = builder.get_object("btnMessageBox")
        self.btnExit = builder.get_object("btnExit")
        self.lblMessage  = builder.get_object("lblMessage")
        self.btnOk = builder.get_object("btnOk")

        # Connect the signals
        builder.connect_signals(self)

        # This global will be used for signaling the thread to stop.
        self.threadStop = 1

        # The thread
        self.thread = ThreadLooper(0.1, self.threadLoop, (1,0,-1))
        self.threadCounter = 0

if __name__ == "__main__":
    # Start GUI instance
    GUI = ThreadGUI()
    GUI.mainWindow.show()
    gtk.main()
Était-ce utile?

La solution

Fileter avec PyGTK est un peu difficile si vous voulez le faire correctement. Fondamentalement, vous ne devez pas mettre à jour l'interface graphique à partir d'un autre thread que le thread principal (limitation commune dans les bibliothèques d'interface graphique). Cela se fait généralement dans PyGTK en utilisant le mécanisme des messages en file d'attente (pour la communication entre les opérateurs et l'interface graphique) qui sont lus périodiquement à l'aide de la fonction de délai d'attente. Une fois que j’ai eu une présentation de mon groupe local de formation sur ce sujet, vous pouvez obtenir un exemple de code pour cette présentation à partir de code Google. référentiel . Examinez la MainWindow classe dans forms/frmmain.py, spécialement pour la méthode _pulse() et ce qui est fait dans on_entry_activate() (le fil de discussion est démarré à cet endroit, plus le minuteur inactif est créé).

def on_entry_activate(self, entry):
    text = entry.get_text().strip()
    if text:
        store = entry.get_completion().get_model()
        if text not in [row[0] for row in store]:
            store.append((text, ))
        thread = threads.RecommendationsFetcher(text, self.queue)# <- 1
        self.idle_timer = gobject.idle_add(self._pulse)# <- 2
        tv_results = self.widgets.get_widget('tv_results')
        model = tv_results.get_model()
        model.clear()
        thread.setDaemon(True)# <- 3
        progress_update = self.widgets.get_widget('progress_update')
        progress_update.show()
        thread.start()# <- 4

De cette façon, l'application met à jour l'interface graphique lorsqu'elle est & "; Idle &"; (par GTK signifie) ne causant pas de gels.

  • 1: créer un fil
  • 2: créer un minuteur inactif
  • 3: démoniser le thread afin que l'application puisse être fermée sans attendre la fin du thread
  • 4: démarrer le fil

Autres conseils

En général, il vaut mieux éviter les threads quand vous le pouvez. Il est très difficile d'écrire correctement une application threadée, et encore plus difficile de savoir que vous avez bien compris. Puisque vous écrivez une application graphique, il est plus facile pour vous de voir comment le faire, car vous devez déjà écrire votre application dans un cadre asynchrone.

La chose importante à réaliser est qu’une application graphique ne fait rien. Il passe le plus clair de son temps à attendre que le système d'exploitation lui dise qu'il se passe quelque chose. Vous pouvez faire beaucoup de choses pendant ce temps mort si vous savez écrire un code long pour qu'il ne bloque pas.

Vous pouvez résoudre votre problème initial en utilisant un délai d'expiration; demandez à votre interface graphique de rappeler une fonction après un délai, puis réinitialisez ce délai ou démarrez un autre appel différé.

Une autre question récurrente est celle de savoir comment communiquer sur le réseau dans une application graphique. Les applications réseau sont comme les applications GUI en ce sens qu'elles font beaucoup d'attente. L'utilisation d'un framework d'E / S réseau (telle que Twisted ) permet de faire en sorte que les deux parties de votre application attendent en mode collaboratif et non en concurrence, et encore réduit le besoin de threads supplémentaires.

Les calculs longs peuvent être écrits de manière itérative au lieu de manière synchrone, et vous pouvez effectuer votre traitement lorsque l'interface graphique est inactive. Vous pouvez utiliser un générateur pour le faire assez facilement en python.

def long_calculation(param, callback):
    result = None
    while True:
        result = calculate_next_part(param, result)
        if calculation_is_done(result):
            break
        else:
            yield
    callback(result)

Appeler long_calculation vous donnera un objet générateur et appeler .next() sur cet objet lancera le générateur jusqu'à ce qu'il atteigne yield ou return. Il vous suffirait d'indiquer au framework d'interface graphique d'appeler long_calculation(some_param, some_callback).next quand il en a le temps et votre callback sera éventuellement appelé avec le résultat.

Je ne connais pas très bien GTK, je ne peux donc pas vous dire quelles fonctions de gobject vous devriez appeler. Avec cette explication, cependant, vous devriez pouvoir trouver les fonctions nécessaires dans la documentation ou, au pire, demander sur un canal IRC approprié.

Malheureusement, il n’existe pas de bonne réponse générale. Si vous clarifiez avec exactement ce que vous essayez de faire, il serait plus facile d’expliquer pourquoi vous n’avez pas besoin de threads dans cette situation.

Vous ne pouvez pas redémarrer un objet de thread arrêté; n'essaye pas. Au lieu de cela, créez une nouvelle instance de l'objet si vous souhaitez le redémarrer après l'avoir réellement arrêté et rejoint.

J'ai joué avec différents outils pour nettoyer le travail avec les threads, le traitement inactif, etc.

make_idle est un décorateur de fonctions qui vous permet d’exécuter une tâche en arrière-plan en coopération. Il s'agit d'un bon compromis entre une tâche suffisamment courte pour s'exécuter une fois dans le fil de l'interface utilisateur et n'affectant pas la réactivité de l'application et une tâche complète en synchronisation spéciale. A l’intérieur de la fonction décorée, vous utilisez & "; Quotient &"; pour renvoyer le traitement à l'interface graphique afin qu'il puisse rester réactif et que la prochaine fois que l'interface utilisateur sera inactive, elle reprendra votre fonction là où vous l'avez laissée. Donc, pour commencer, appelez simplement idle_add à la fonction décorée.

def make_idler(func):
    """
    Decorator that makes a generator-function into a function that will
continue execution on next call
    """
    a = []

    @functools.wraps(func)
    def decorated_func(*args, **kwds):
        if not a:
            a.append(func(*args, **kwds))
        try:
            a[0].next()
            return True
        except StopIteration:
            del a[:]
            return False

    return decorated_func

Si vous devez effectuer un peu plus de traitement, vous pouvez utiliser un gestionnaire de contexte pour verrouiller le thread d'interface utilisateur chaque fois que cela est nécessaire pour rendre le code un peu plus sûr.

@contextlib.contextmanager
def gtk_critical_section():
    gtk.gdk.threads_enter()
    try:
        yield
    finally:
        gtk.gdk.threads_leave()

avec ça vous pouvez juste

with gtk_critical_section():
    ... processing ...

Je n'en ai pas encore fini, mais en combinant des actions purement inactives et purement dans un fil, j'ai un décorateur (pas encore testé, donc pas posté) qui vous permet de dire si la prochaine section après le rendement est être exécuté dans le temps d'inactivité de l'interface utilisateur ou dans un thread. Cela permettrait de faire une configuration dans le thread d'interface utilisateur, de passer à un nouveau thread pour effectuer des tâches en arrière-plan, puis de basculer vers le temps d'inactivité de l'interface utilisateur pour effectuer un nettoyage, minimisant ainsi le besoin de verrous.

Je n'ai pas examiné votre code en détail. Mais je vois deux solutions à votre problème:

N'utilisez pas de fil du tout. Utilisez plutôt un délai d’attente, comme ceci:

import gobject

i = 0
def do_print():
    global i
    print i
    i += 1
    if i == 10:
        main_loop.quit()
        return False
    return True

main_loop = gobject.MainLoop()
gobject.timeout_add(250, do_print)
main_loop.run()

Lorsque vous utilisez des threads, vous devez vous assurer que votre code d'interface graphique n'est appelé qu'à partir d'un thread à la fois en le protégeant comme suit:

import threading
import time

import gobject
import gtk

gtk.gdk.threads_init()

def run_thread():
    for i in xrange(10):
        time.sleep(0.25)
        gtk.gdk.threads_enter()
        # update the view here
        gtk.gdk.threads_leave()
    gtk.gdk.threads_enter()
    main_loop.quit()
    gtk.gdk.threads_leave()

t = threading.Thread(target=run_thread)
t.start()
main_loop = gobject.MainLoop()
main_loop.run()
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top