Cómo realizar un seguimiento del progreso de rosca en Python sin congelar el GUI PyQt?
-
05-09-2019 - |
Pregunta
Preguntas:
- ¿Cuál es la mejor práctica para hacer el seguimiento de una banda de rodadura de el progreso sin bloquear la interfaz gráfica de usuario ( "No responde")?
- En general, ¿cuáles son las mejores prácticas para enhebrar como se aplica a GUI el desarrollo?
Cuestión de fondo:
- Tengo una interfaz gráfica de usuario PyQt para Windows.
- Se utiliza para procesar conjuntos de HTML documentos.
- Se tarda de tres segundos a tres horas para procesar un conjunto de documentos.
- Quiero ser capaz de procesar múltiples juegos al mismo tiempo.
- No quiero que la interfaz gráfica de usuario para bloquear.
- Busco en el módulo threading para lograrlo.
- Soy relativamente nuevo en el roscado.
- La interfaz gráfica de usuario tiene una barra de progreso.
- quiero que se muestre el progreso de el hilo seleccionado.
- mostrar los resultados de la seleccionada hilo si está terminado.
- Estoy usando Python 2.5.
Mi idea: Haga que los hilos de emitir una QtSignal cuando se actualiza el progreso que desencadena alguna función que actualiza la barra de progreso. También señal cuando terminado de procesar lo que los resultados se pueden visualizar.
#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)
será apreciado Cualquier comentario sobre este enfoque (por ejemplo, inconvenientes, las trampas, alabanzas, etc.).
Resolución:
Terminé usando la clase QThread y señales asociadas y ranuras para la comunicación entre hilos. Esto es principalmente porque mi programa ya utiliza Qt / PyQt4 de los objetos GUI / widgets. Esta solución también requiere un menor número de cambios en mi código existente para poner en práctica.
Aquí hay un enlace a un artículo aplicable Qt Qt que explica cómo maneja los hilos y señales, http: // www.linuxjournal.com/article/9602 . Extracto a continuación:
Afortunadamente, permisos de Qt señales y ranuras para ser conectados a través de hilos, siempre y cuando los hilos se están ejecutando sus propios bucles de eventos. Este es un método mucho más limpio de la comunicación en comparación con el envío y la la recepción de eventos, ya que evita toda la contabilidad y el intermedio clases QEvent derivados que se convierten necesario en cualquier trivial solicitud. La comunicación entre las discusiones ahora se convierte en una cuestión de la conexión de señales de un subproceso a las ranuras en otro, y la mutexing y cuestiones de hilo de seguridad de intercambio datos entre hilos son manejados por Qt.
¿Por qué es necesario ejecutar un evento bucle dentro de cada hilo al que se desee conectar señales? La razón tiene que ver con la inter-hilo mecanismo de comunicación utilizado por Qt cuando la conexión de señales de una hilo para la ranura de otro hilo. Cuando se realiza una conexión de este tipo, es se hace referencia como una conexión en cola. Cuando las señales se emiten a través de una en cola de conexión, la ranura se invoca la próxima vez que el objeto de destino se ejecuta bucle de eventos. Si la ranura en su lugar había sido invocada directamente por una señal de otro hilo, que ranura ejecutaría en el mismo contexto que el subproceso de llamada. Normalmente, esto es no lo que quiere (y sobre todo no lo que quiere si está utilizando una conexión de base de datos, como la base de datos conexión puede ser utilizado sólo por el hilo que lo creó). la cola conexión despacha adecuadamente el indicar al objeto hilo y invoca su ranura en su propio contexto por piggy-respaldo en el sistema de eventos. Esto es precisamente lo que queremos para comunicación entre hilos en la que algunos de los hilos están manejando conexiones de bases de datos. el Qt mecanismo de señal / ranura está en una raíz aplicación de la inter-hilo esquema de eventos que pasa señalado anteriormente, pero con un mucho más limpio y más fácil de usar interfaz.
Nota: eliben también tiene una buena respuesta, y si yo no estuviera usando PyQt4, que se ocupa de hilo de seguridad y mutexing, la solución habría sido mi elección.
Solución
Si desea utilizar señales para indicar el progreso para el hilo principal, entonces realmente debe utilizar la clase QThread de PyQt en lugar de la clase Thread del módulo threading de Python.
Un ejemplo simple que utiliza QThread, señales y las ranuras se puede encontrar en la PyQt Wiki:
https://wiki.python.org/moin/PyQt/Threading,_Signals_and_Slots
Otros consejos
colas nativa de Python no funcionarán porque hay que bloquear el encendido consigue cola (), que los tapones hasta la interfaz de usuario.
Qt implementa esencialmente un sistema de colas en el interior para la comunicación hilo cruzado. Prueba esta llamada desde cualquier hilo para colocar una llamada a una ranura.
QtCore.QMetaObject.invokeMethod ()
Es torpe y no está bien documentado, pero debe hacer lo que quiera, incluso a partir de un hilo no Qt.
También puede utilizar la maquinaria para este evento. Ver QApplication (o QCoreApplication) por un método llamado algo así como "post".
Edit: He aquí un ejemplo más completo ...
He creado mi propia clase basado en QWidget. Tiene una ranura que acepta una cadena; La defino como esto:
@QtCore.pyqtSlot(str)
def add_text(self, text):
...
Más tarde, se crea una instancia de este widget en el hilo principal de GUI. Desde el hilo principal GUI o cualquier otro hilo (toco madera) Puedo llamar a:
QtCore.QMetaObject.invokeMethod(mywidget, "add_text", QtCore.Q_ARG(str,"hello world"))
Clunky, pero te lleva allí.
Dan.
Yo recomiendo usar cola en lugar de la señalización. En lo personal me parece una manera mucho más robusta y comprensible de la programación, porque es más sincrónica.
Hilos deben recibir "trabajos" de una cola, y volver a poner los resultados en otra cola. Sin embargo, un tercio de la cola puede ser utilizado por los hilos de notificaciones y mensajes, como los errores y los "informes sobre la marcha". Una vez que la estructura de su código de esta manera, se hace mucho más fácil de manejar.
De esta manera, un solo "cola de trabajos" y "cola resultado" también pueden ser utilizados por un grupo de subprocesos de trabajo, encamina toda la información de los hilos en el hilo principal interfaz gráfica de usuario.
Si su método de "processDoc" no cambia ningún otro dato (sólo se ve a algunos datos y devolverlo y no cambian las variables o propiedades de la clase padre) se puede utilizar Py_BEGIN_ALLOW_THREADS y Py_END_ALLOW_THREADS macroses ( ver aquí para más detalles ) en ella. Por lo que el documento se procesa de hilo que no se bloqueará el intérprete y la interfaz de usuario será actualizada.
Siempre va a tener este problema en Python. Google GIL "bloqueo intérprete global" para obtener más antecedentes. Hay dos formas generalmente recomendadas para evitar el problema que está experimentando: usar Twisted , o utilizar un módulo similar a la multiprocesamiento módulo introducido en 2.5.
Twisted requerirá que se aprende técnicas de programación asíncrona que puede ser confuso al principio, pero será útil si alguna vez necesita para escribir aplicaciones de red de alto rendimiento y será más beneficioso para usted en el largo plazo.
El módulo de multiprocesamiento se bifurcará un nuevo proceso y utiliza IPC para hacer que se comporte como si tuviera cierto roscado. El único inconveniente es que lo que se necesita Python 2.5 instalado, que es bastante nuevo y inst' incluido en la mayoría de distribuciones de Linux o OSX por defecto.