PySide / PyQt – Al iniciar un subproceso intensivo de CPU, se bloquea toda la aplicación

Estoy intentando hacer algo bastante común en mi aplicación PySide GUI: quiero delegar algunas tareas intensivas de CPU a un subproceso en segundo plano para que mi GUI se mantenga receptiva e incluso pueda mostrar un indicador de progreso a medida que avanza el cálculo.

Esto es lo que estoy haciendo (estoy usando PySide 1.1.1 en Python 2.7, Linux x86_64):

import sys import time from PySide.QtGui import QMainWindow, QPushButton, QApplication, QWidget from PySide.QtCore import QThread, QObject, Signal, Slot class Worker(QObject): done_signal = Signal() def __init__(self, parent = None): QObject.__init__(self, parent) @Slot() def do_stuff(self): print "[thread %x] computation started" % self.thread().currentThreadId() for i in range(30): # time.sleep(0.2) x = 1000000 y = 100**x print "[thread %x] computation ended" % self.thread().currentThreadId() self.done_signal.emit() class Example(QWidget): def __init__(self): super(Example, self).__init__() self.initUI() self.work_thread = QThread() self.worker = Worker() self.worker.moveToThread(self.work_thread) self.work_thread.started.connect(self.worker.do_stuff) self.worker.done_signal.connect(self.work_done) def initUI(self): self.btn = QPushButton('Do stuff', self) self.btn.resize(self.btn.sizeHint()) self.btn.move(50, 50) self.btn.clicked.connect(self.execute_thread) self.setGeometry(300, 300, 250, 150) self.setWindowTitle('Test') self.show() def execute_thread(self): self.btn.setEnabled(False) self.btn.setText('Waiting...') self.work_thread.start() print "[main %x] started" % (self.thread().currentThreadId()) def work_done(self): self.btn.setText('Do stuff') self.btn.setEnabled(True) self.work_thread.exit() print "[main %x] ended" % (self.thread().currentThreadId()) def main(): app = QApplication(sys.argv) ex = Example() sys.exit(app.exec_()) if __name__ == '__main__': main() 

La aplicación muestra una sola ventana con un botón. Cuando se presiona el botón, espero que se desactive mientras se realiza el cálculo. Entonces, el botón debe ser habilitado nuevamente.

Lo que sucede, en cambio, es que cuando presiono el botón, toda la ventana se congela mientras se realiza el cálculo y luego, cuando termina, recupero el control de la aplicación. El botón nunca parece estar deshabilitado. Algo extraño que noté es que si reemplazo el cálculo intensivo de CPU en do_stuff() con un simple time.sleep (), el progtwig se comporta como se esperaba.

No sé exactamente qué está pasando, pero parece que la prioridad del segundo subproceso es tan alta que en realidad impide que el subproceso de la GUI se programe. Si el segundo subproceso pasa al estado BLOQUEADO (como ocurre con un sleep() ), la GUI tiene la oportunidad de ejecutarse y actualiza la interfaz como se espera. Intenté cambiar la prioridad del subproceso de trabajo, pero parece que no se puede hacer en Linux.

Además, trato de imprimir los ID de hilo, pero no estoy seguro de si lo estoy haciendo correctamente. Si lo soy, la afinidad del hilo parece ser correcta.

También probé el progtwig con PyQt y el comportamiento es exactamente el mismo, de ahí las tags y el título. Si puedo hacerlo funcionar con PyQt4 en lugar de PySide, podría cambiar mi aplicación completa a PyQt4

Esto probablemente es causado por el subproceso de trabajo que contiene GIL de Python. En algunas implementaciones de Python, solo se puede ejecutar un hilo de Python a la vez. El GIL evita que otros subprocesos ejecuten el código de Python, y se libera durante las llamadas de función que no necesitan el GIL.

Por ejemplo, la GIL se libera durante la IO real, ya que la IO es manejada por el sistema operativo y no por el intérprete de Python.

Soluciones:

  1. Aparentemente, puede usar time.sleep(0) en su subproceso de trabajo para ceder a otros subprocesos (de acuerdo con esta pregunta SO ). Tendrá que llamar a time.sleep(0) periódicamente, y el subproceso de la GUI solo se ejecutará mientras el subproceso de fondo esté llamando a esta función.

  2. Si el subproceso de trabajo es lo suficientemente autónomo, puede colocarlo en un proceso completamente separado y luego comunicarse mediante el envío de objetos encurtidos a través de tuberías. En el proceso de primer plano, cree un subproceso de trabajo para hacer IO con el proceso en segundo plano. Dado que el subproceso de trabajo realizará IO en lugar de las operaciones de la CPU, no mantendrá la GIL y esto le dará un subproceso de la GUI completamente sensible.

  3. Algunas implementaciones de Python (JPython y IronPython) no tienen un GIL.

Los subprocesos en CPython solo son realmente útiles para multiplexar las operaciones de E / S, no para poner en segundo plano las tareas intensivas de CPU. Para muchas aplicaciones, el subproceso en la implementación de CPython está fundamentalmente interrumpido y es probable que se mantenga así en el futuro inmediato.

Al final, esto funciona para mi problema, y ​​el código puede ayudar a otra persona.

 import sys from PySide import QtCore, QtGui import time class qOB(QtCore.QObject): send_data = QtCore.Signal(float, float) def __init__(self, parent = None): QtCore.QObject.__init__(self) self.parent = None self._emit_locked = 1 self._emit_mutex = QtCore.QMutex() def get_emit_locked(self): self._emit_mutex.lock() value = self._emit_locked self._emit_mutex.unlock() return value @QtCore.Slot(int) def set_emit_locked(self, value): self._emit_mutex.lock() self._emit_locked = value self._emit_mutex.unlock() @QtCore.Slot() def execute(self): t2_z = 0 t1_z = 0 while True: t = time.clock() if self.get_emit_locked() == 1: # cleaner #if self._emit_locked == 1: # seems a bit faster but less responsive, t1 = 0.07, t2 = 150 self.set_emit_locked(0) self.send_data.emit((t-t1_z)*1000, (t-t2_z)*1000) t2_z = t t1_z = t class window(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) self.l = QtGui.QLabel(self) self.l.setText("eins") self.l2 = QtGui.QLabel(self) self.l2.setText("zwei") self.l2.move(0, 20) self.show() self.q = qOB(self) self.q.send_data.connect(self.setLabel) self.t = QtCore.QThread() self.t.started.connect(self.q.execute) self.q.moveToThread(self.t) self.t.start() @QtCore.Slot(float, float) def setLabel(self, inp1, inp2): self.l.setText(str(inp1)) self.l2.setText(str(inp2)) self.q.set_emit_locked(1) if __name__ == '__main__': app = QtGui.QApplication(sys.argv) win = window() sys.exit(app.exec_())