Comment assurer le suivi des progrès de fil en Python sans geler l'interface graphique PyQt?
-
05-09-2019 - |
Question
Questions:
- Quelle est la meilleure pratique pour garder une trace de ce une bande de roulement progrès sans verrouillage de l'interface ( "Ne répond pas")?
- En général, quelles sont les meilleures pratiques pour threading comme il applique à l'interface graphique développement?
Question de fond:
- J'ai une interface graphique PyQt pour Windows.
- Il est utilisé pour traiter des ensembles de HTML documents.
- Il faut de trois secondes à trois heures pour traiter un ensemble de documents.
- Je veux être en mesure de traiter plusieurs jeux en même temps.
- Je ne veux pas l'interface graphique pour verrouiller.
- Je suis à la recherche sur le module de filetage pour y parvenir.
- Je suis relativement nouveau à filetage.
- L'interface graphique a une barre de progression.
- Je veux afficher la progression de le fil sélectionné.
- Afficher les résultats du sélectionné fil si elle est terminée.
- J'utilise Python 2.5.
Mon Idée: Demandez aux fils émettre un QtSignal lorsque le progrès est mis à jour qui déclenche une fonction qui met à jour la barre de progression. signaler également lorsque le traitement terminé si 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 relatif à cette approche (par exemple inconvénients, pièges, louanges, etc.) seront appréciés.
Résolution:
I fini en utilisant la classe QThread et de signaux associés et des fentes pour la communication entre threads. Ceci est principalement parce que mon programme utilise déjà Qt / PyQt4 pour les objets GUI / widgets. Cette solution doit également moins de changements à mon code existant pour mettre en œuvre.
Voici un lien vers un article Qt applicable qui explique comment threads gère Qt et signaux, http: // www.linuxjournal.com/article/9602 . Extrait ci-dessous:
Heureusement, Qt permet signaux et des fentes à raccorder à travers des fils-aussi longtemps que les fils sont en cours d'exécution de leurs propres boucles d'événements. Cette méthode est beaucoup plus propre de communication par rapport à l'envoi et la réception d'événements, car elle évite toute la comptabilité et intermédiaire classes dérivées QEvent qui deviennent nécessaire dans toute non triviale application. communiquer entre fils devient maintenant une question de connecter les signaux d'un fil à les fentes de l'autre, et le mutexing et les questions d'échange fil de sécurité données entre les fils sont traités par Qt.
Pourquoi est-il nécessaire d'exécuter un événement boucle dans chaque fil auquel vous veulent connecter les signaux? La raison doit faire avec l'inter-thread mécanisme de communication utilisé par Qt lors de la connexion des signaux provenant d'une le fil dans la fente d'un autre fil. Lorsqu'une telle connexion est établie, il est appelé une connexion en file d'attente. Lorsque les signaux sont émis à travers un connexion en file d'attente, la fente est invoquée la prochaine fois de l'objet de destination boucle d'événements est exécutée. Si la fente avait plutôt été invoquée directement par un signal provenant d'un autre fil, cette fente exécuterait dans le même contexte que le fil d'appel. Normalement, cela est pas ce que vous voulez (et surtout pas ce que vous voulez si vous utilisez un connexion de base de données, la base de données connexion peut être utilisé que par le fil qui a créé). la file d'attente connexion envoie correctement la signaler à l'objet de fil et invoque son emplacement dans son contexte par -Support porcin sur le système d'événements. C'est précisément ce que nous voulons communication inter-thread dans lequel certains des fils sont la manipulation connexions de base de données. Qt Mécanisme de signal / de la fente est à une racine la mise en oeuvre de l'inter-thread système de passage de l'événement décrit ci-dessus, mais avec beaucoup plus propre et l'interface plus facile à utiliser.
Remarque: eliben a aussi une bonne réponse, et si je ne l'utilise PyQt4, qui gère la sécurité des threads et mutexage, sa solution aurait été mon choix.
La solution
Si vous voulez utiliser des signaux pour indiquer la progression du thread principal alors vous devriez vraiment utiliser la classe QThread PyQt au lieu de la classe de fil à partir du module de filetage de Python.
Un exemple simple qui utilise QThread, signaux et slots se trouve sur le PyQt Wiki:
https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots
Autres conseils
files d'attente python natif ne fonctionnera pas parce que vous devez bloquer sur get () de file d'attente, qui bondes votre interface utilisateur.
Qt met en oeuvre essentiellement un système de file d'attente à l'intérieur pour la communication de fil croix. Essayez cet appel de tout fil pour faire un appel à une fente.
QtCore.QMetaObject.invokeMethod ()
Il est maladroit et mal documenté, mais il doit faire ce que vous voulez même d'un fil de 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 chaîne; Je 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 de l'interface principale. A partir du thread principal GUI ou tout autre fil (touchons du bois) Je peux appeler:
QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))
Maladroit, mais il vous arrive.
Dan.
Je vous recommande d'utiliser la file d'attente au lieu de la signalisation. Personnellement, je trouve beaucoup plus robuste et compréhensible façon de la programmation, car il est plus synchrone.
Threads devraient obtenir « emplois » à partir d'une file d'attente et remettre les résultats sur une autre file d'attente. Une troisième file d'attente peut être utilisé par les fils pour les notifications et messages, comme des erreurs et des « rapports d'étape ». 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 d'emploi » et « résultat de file d'attente » peuvent également être utilisés par un groupe de threads de travail, il achemine toutes les informations des fils dans le fil de l'interface principale.
Si votre méthode « processDoc » ne change pas d'autres données (pour certaines regarde juste les données et le retourner et ne changent pas les variables ou les propriétés de la classe parent), vous pouvez utiliser Py_BEGIN_ALLOW_THREADS et Py_END_ALLOW_THREADS macroses ( voir ici pour plus de détails ) en elle. Ainsi, le document sera traité en fil qui ne se verrouille pas l'interprète et l'interface utilisateur sera mis à jour.
Vous allez toujours avoir ce problème en Python. Google GIL "verrou global interpretor" pour plus d'arrière-plan. Il y a deux façons généralement recommandées pour contourner le problème que vous rencontrez: utilisez Twisted , ou utiliser un module similaire à la multitraitement introduite dans le module 2.5.
Twisted exigera que vous apprendrez les techniques de programmation asynchrone qui peut être source de confusion au début, mais sera utile si vous avez besoin d'écrire des applications de réseau à haut débit et sera plus bénéfique pour vous à long terme.
Le module multiprocessing se divisera un nouveau processus et utilise IPC pour le faire se comporter comme si vous aviez vrai filetage. Seul inconvénient est que vous auriez besoin Python 2.5 installé, ce qui est assez nouveau et inst » inclus dans la plupart des distributions Linux ou Mac OS X par défaut.