2012-06-29 38 views
11

Estoy intentando hacer algo bastante común en mi aplicación PySide GUI: quiero delegar algunas tareas intensivas de CPU en un hilo de fondo para que mi GUI se mantenga receptivo e incluso podría mostrar un indicador de progreso a medida que avanza el cálculo.PySide/PyQt - Iniciar un hilo intensivo de CPU bloquea toda la aplicación

Esto es lo que estoy haciendo (estoy usando PySide 1.1.1 de 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 deshabilite mientras se realiza el cálculo. Entonces, el botón debería ser reactivado.

Lo que ocurre, en cambio, es que cuando presiono el botón toda la ventana se congela mientras el cálculo continúa y luego, cuando termina, recupero el control de la aplicación. El botón nunca parece estar deshabilitado. Una cosa graciosa que noté es que si reemplazo el cálculo intensivo de CPU en do_stuff() con un simple time.sleep() el programa 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 se programe alguna vez el subproceso de GUI. Si el segundo hilo entra en estado BLOQUEADO (como ocurre con un sleep()), la GUI tiene la posibilidad de ejecutar y actualizar la interfaz como se esperaba. Traté de 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 estoy, la afinidad del hilo parece ser correcta.

También probé el programa con PyQt y el comportamiento es exactamente el mismo, de ahí las etiquetas y el título. Si puedo hacer que funcione con PyQt4 en lugar de PySide, podría cambiar toda mi aplicación a PyQt4

Respuesta

12

Esto probablemente sea causado por el hilo de trabajo que contiene el GIL de Python. En algunas implementaciones de Python, solo se puede ejecutar un hilo de Python a la vez. El GIL impide 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, el GIL se libera durante IO real, ya que IO es manejado por el sistema operativo y no por el intérprete de Python.

Soluciones:

  1. Al parecer, se puede utilizar time.sleep(0) en su subproceso de trabajo a ceder a otros hilos (according to this SO question). Deberá llamar periódicamente al time.sleep(0), y el hilo de la GUI solo se ejecutará mientras el hilo de fondo llame a esta función.

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

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

Temas en CPython solamente son realmente útiles para las operaciones de IO de multiplexación, no para poner tareas intensivas de la CPU en el fondo. Para muchas aplicaciones, el enhebrado en la implementación de CPython está fundamentalmente roto y es probable que siga así en el futuro previsible.

+0

Intenté poner 'time.sleep (0)' en lugar del comentario 'time.sleep (0.2)' en el ejemplo y, desafortunadamente, no soluciona el problema. Noté que con valores como 'time.sleep (0.05)' el botón se comporta como se esperaba. Como solución alternativa, podría configurar el trabajador para informar el progreso solo unas pocas veces por segundo y dormir para permitir que la GUI se actualice solo. – user1491306

+1

Hay soluciones para subprocesos y GUI (como http://code.activestate.com/recipes/578154). –

+0

Terminé haciendo esto: puse un 'sleep' donde comienza el cálculo, y cada vez que el indicador de progreso se actualiza, llamo a' app.processEvents() 'para hacer que funcione un botón" Abort ". – user1491306

0

al final esto funciona para mi problema, por lo que 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_()) 
Cuestiones relacionadas