Comment garder une trace de la progression du thread en Python sans geler L'interface graphique PyQt?

Questions:

  1. Quelle est la meilleure pratique pour garder la trace d'une bande de roulement progresser sans verrouiller L'interface graphique ("Pas De Réponse")?
  2. en général, quelles sont les meilleures pratiques filetage tel qu'il s'applique à GUI le développement?

Question Contexte:

  • j'ai une interface graphique PyQt Pour Windows.
  • il est utilisé pour traiter des ensembles de HTML document.
  • il faut n'importe où de trois secondes à trois heures pour traiter un ensemble de document.
  • je veux être capable de traiter plusieurs jeux en même temps.
  • Je ne veux pas que l'interface graphique soit verrouillée.
  • je regarde le module de filetage pour atteindre cet objectif.
  • je suis relativement nouveau au filetage.
  • le GUI a un la barre de progression.
  • je veux qu'il affiche la progression de le fil sélectionné.
  • affiche les résultats de la sélection thread si c'est fini.
  • J'utilise Python 2.5.

Mon Idée: Ont les fils émettent un QtSignal lorsque la progression est mise à jour qui déclenche une fonction qui met à jour la barre de progression. Signal également lorsque le traitement terminé de sorte que les résultats peuvent être affichés.

#NOTE: this is example code for my idea, you do not have
#      to read this to answer the question(s).

import threading
from PyQt4 import QtCore, QtGui
import re
import copy

class ProcessingThread(threading.Thread, QtCore.QObject):

    __pyqtSignals__ = ( "progressUpdated(str)",
                        "resultsReady(str)")

    def __init__(self, docs):
        self.docs = docs
        self.progress = 0   #int between 0 and 100
        self.results = []
        threading.Thread.__init__(self)

    def getResults(self):
        return copy.deepcopy(self.results)

    def run(self):
        num_docs = len(self.docs) - 1
        for i, doc in enumerate(self.docs):
            processed_doc = self.processDoc(doc)
            self.results.append(processed_doc)
            new_progress = int((float(i)/num_docs)*100)

            #emit signal only if progress has changed
            if self.progress != new_progress:
                self.emit(QtCore.SIGNAL("progressUpdated(str)"), self.getName())
            self.progress = new_progress
            if self.progress == 100:
                self.emit(QtCore.SIGNAL("resultsReady(str)"), self.getName())

    def processDoc(self, doc):
        ''' this is tivial for shortness sake '''
        return re.findall('<a [^>]*>.*?</a>', doc)


class GuiApp(QtGui.QMainWindow):

    def __init__(self):
        self.processing_threads = {}  #{'thread_name': Thread(processing_thread)}
        self.progress_object = {}     #{'thread_name': int(thread_progress)}
        self.results_object = {}      #{'thread_name': []}
        self.selected_thread = ''     #'thread_name'

    def processDocs(self, docs):
        #create new thread
        p_thread = ProcessingThread(docs)
        thread_name = "example_thread_name"
        p_thread.setName(thread_name)
        p_thread.start()

        #add thread to dict of threads
        self.processing_threads[thread_name] = p_thread

        #init progress_object for this thread
        self.progress_object[thread_name] = p_thread.progress  

        #connect thread signals to GuiApp functions
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('progressUpdated(str)'), self.updateProgressObject(thread_name))
        QtCore.QObject.connect(p_thread, QtCore.SIGNAL('resultsReady(str)'), self.updateResultsObject(thread_name))

    def updateProgressObject(self, thread_name):
        #update progress_object for all threads
        self.progress_object[thread_name] = self.processing_threads[thread_name].progress

        #update progress bar for selected thread
        if self.selected_thread == thread_name:
            self.setProgressBar(self.progress_object[self.selected_thread])

    def updateResultsObject(self, thread_name):
        #update results_object for thread with results
        self.results_object[thread_name] = self.processing_threads[thread_name].getResults()

        #update results widget for selected thread
        try:
            self.setResultsWidget(self.results_object[thread_name])
        except KeyError:
            self.setResultsWidget(None)

tout commentaire sur cette approche (par exemple inconvénients, pièges, louanges, etc.) sera appréciée.

résolution:

j'ai fini par utiliser la classe QThread et les signaux associés et les fentes pour communiquer entre les fils. Ceci est principalement dû au fait que mon programme utilise déjà Qt/PyQt4 pour les objets/widgets GUI. Cette solution nécessitait également moins de modifications à mon code existant pour être mise en œuvre.

voici un lien vers un article QT applicable qui explique comment Qt gère les threads et les signaux, http://www.linuxjournal.com/article/9602 . Extrait ci-dessous:

heureusement, QT permet signaux et emplacements à connecter à travers les fils-aussi longtemps que les fils sont en train d'exécuter leurs propres boucles d'événements. C'est beaucoup plus propre méthode de la communication par rapport à l'envoi et recevoir des événements, parce qu'il éviter toute la comptabilité et intermédiaires Les classes dérivées de QEvent qui deviennent nécessaire dans tout non trivial application. La communication entre fils devient maintenant une question de les signaux de connexion d'un fil à les fentes dans une autre, et le mutexing et les questions de sécurité du fil de l'échange les données entre les threads sont gérées par Qt.

Pourquoi est-il nécessaire de gérer un événement boucle à l'intérieur de chaque fil auquel vous souhaitez connecter des signaux? Raison a à voir avec l'inter-thread mécanisme de communication utilisé par Qt lors de la connexion de signaux à partir d'un filez dans la fente d'un autre fil. Lorsqu'une connexion est établie, il est désigné comme une file d'attente de connexion. Lorsque des signaux sont émis par l'intermédiaire d'un connexion en file d'attente, la fente est invoquée la prochaine fois que l'objet de destination la boucle d'événement est exécutée. Si la fente cela avait été invoquée directement par un signal d'un autre fil, Cette fente exécuté dans le même contexte comme le thread appelant. Normalement, c'est pas ce que vous voulez (et surtout pas ce que vous voulez si vous utilisez un connexion de base de données, comme la base de données la connexion ne peut être utilisée que par le fil qui l'a créé). La file d'attente la connexion expédie correctement signal à l'objet thread et invoque sa fente dans son propre contexte par piggy-backing sur le système d'événements. C'est précisément ce que nous voulons pour communication inter-thread dans laquelle certains fils sont manipulés connexions de base de données. L'Intervalle Qt le mécanisme de signal/fente est à la racine la mise en œuvre de l'inter-thread schéma de passage d'un événement décrit ci-dessus, mais avec une beaucoup plus propre et plus facile à utiliser l'interface.

NOTE: eliben a aussi une bonne réponse, et si je n'utilisais pas PyQt4, qui gère la sécurité du fil et le mutexing, sa solution aurait été mon choix.

19
demandé sur tgray 2009-02-20 17:00:06

5 réponses

si vous voulez utiliser des signaux pour indiquer la progression vers le thread principal, alors vous devriez vraiment utiliser la classe QThread de PyQt au lieu de la classe Thread du module threading de Python.

un exemple simple qui utilise QThread, signaux et fentes peut être trouvé sur le Wiki de PyQt:

https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots

10
répondu David Boddie 2014-12-16 15:30:09

les files D'attente natives python ne fonctionneront pas parce que vous devez bloquer la file d'attente get(), ce qui retarde votre UI.

Qt implémente essentiellement un système de mise en file d'attente à l'intérieur pour la communication entre fils croisés. Essayez cet appel à partir de n'importe quel fil pour poster un appel à une fente.

QtCore.QMetaObject.invokeMethod ()

c'est grotesque et est mal documenté, mais il devrait faire ce que vous voulez même à partir d'un fil non-Qt.

vous pouvez également utiliser des machines d'événement pour cela. Voir QApplication (ou QCoreApplication) pour une méthode nommée quelque chose comme "post".

Edit: voici un exemple plus complet...

j'ai créé ma propre classe basée sur QWidget. Il a une fente qui accepte une corde; je la définis comme ceci:

@QtCore.pyqtSlot(str)
def add_text(self, text):
   ...

plus tard, je crée une instance de ce widget dans le fil GUI principal. À partir du fil GUI principal ou de tout autre fil (frapper sur bois) je peux appeler:

QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))

Clunky, mais il vous amène là.

Dan.

5
répondu 2009-06-22 14:56:20

je vous recommande D'utiliser la file d'attente au lieu de la signalisation. Personnellement, je trouve ça un beaucoup plus robuste et compréhensible de la programmation, parce que c'est plus synchrone.

Threads devrait obtenir "jobs" d'une file d'attente, et de remettre les résultats sur une autre File. Pourtant, une troisième file d'attente peut être utilisée par les threads pour les notifications et les messages, comme les erreurs et les "rapports de progrès". Une fois que vous structurez votre code de cette façon, il devient beaucoup plus simple à gérer.

De cette façon, une seule" file d'attente de travail "et" file d'attente de résultat " peut aussi être utilisée par un groupe de threads de worker, il achemine toutes les informations des threads dans le thread GUI principal.

4
répondu Eli Bendersky 2009-02-21 07:18:06

si votre méthode" processDoc "ne change aucune autre donnée (il suffit de rechercher quelques données et de les retourner et de ne pas changer les variables ou les propriétés de la classe mère), vous pouvez utiliser Py_BEGIN_ALLOW_THREADS et Py_END_ALLOW_THREADS macroses ( voir ici pour plus de détails ). Ainsi, le document sera traité dans un thread qui ne verrouillera pas l'interpréteur et L'interface utilisateur sera mise à jour.

1
répondu zihotki 2009-02-21 05:44:14

Vous allez toujours avoir ce problème en Python. Google GIL "global interpretor lock" pour plus d'arrière-plan. Il y a deux moyens généralement recommandés pour contourner le problème que vous rencontrez: utilisez Twisted , ou utilisez un module similaire au multiprocessing module présenté en 2.5.

Twisted vous demandera d'apprendre des techniques de programmation asynchrone qui peuvent être déroutantes au début mais sera utile si vous avez besoin d'écrire un haut débit réseau apps et sera plus avantageux pour vous dans le long terme.

le module multiprocessing va bifurquer un nouveau processus et utilise la CIB pour le faire se comporter comme si vous aviez un vrai threading. Le seul inconvénient est que vous aurez besoin de python 2.5 installé qui est assez nouveau et inst' inclus dans la plupart des distributions Linux ou OSX par défaut.

0
répondu MrEvil 2009-02-20 18:43:44