2012-06-29 5 views
11

Sto cercando di fare una cosa abbastanza comune nella mia applicazione PySide GUI: Voglio delegare alcune attività CPU-Intensive a un thread in background in modo che la mia GUI rimanga reattivo e potrebbe anche visualizzare un indicatore di progresso come il calcolo va.PySide/PyQt - Avvio di un thread intensivo della CPU blocca l'intera applicazione

Ecco quello che sto facendo (sto usando PySide 1.1.1 su 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() 

L'applicazione visualizza una singola finestra con un pulsante. Quando si preme il pulsante, mi aspetto che si disattivi mentre viene eseguito il calcolo. Quindi, il pulsante dovrebbe essere riattivato.

Quello che succede, invece, è che quando premo il pulsante l'intera finestra si blocca mentre il calcolo va e poi, quando è finito, riprendo il controllo dell'applicazione. Il pulsante non sembra mai disabilitato. Una cosa divertente che ho notato è che se sostituisco il calcolo intensivo della CPU in do_stuff() con un semplice time.sleep() il programma si comporta come previsto.

Non so esattamente cosa sta succedendo, ma sembra che la priorità del secondo thread sia così alta da impedire effettivamente che il thread della GUI venga programmato. Se il secondo thread va in stato BLOCCATO (come accade con un sleep()), la GUI ha effettivamente la possibilità di eseguire e aggiornare l'interfaccia come previsto. Ho provato a cambiare la priorità del thread di lavoro, ma sembra che non possa essere fatto su Linux.

Inoltre, provo a stampare gli ID di thread, ma non sono sicuro se lo sto facendo correttamente. Se lo sono, l'affinità del thread sembra essere corretta.

Ho anche provato il programma con PyQt e il comportamento è esattamente lo stesso, quindi tag e titolo. Se riesco a farlo funzionare con PyQt4 anziché PySide, potrei passare l'intera applicazione a PyQt4

risposta

12

Ciò è probabilmente causato dal thread worker che contiene GIL di Python. In alcune implementazioni Python, solo un thread Python può essere eseguito alla volta. Il GIL impedisce ad altri thread di eseguire codice Python e viene rilasciato durante chiamate di funzione che non hanno bisogno di GIL.

Ad esempio, il GIL viene rilasciato durante l'I/O effettivo, poiché l'I/O viene gestito dal sistema operativo e non dall'interprete Python.

Solutions:

  1. A quanto pare, è possibile utilizzare time.sleep(0) nel thread di lavoro di cedere ad altri thread (according to this SO question). Dovrai chiamare periodicamente lo time.sleep(0) e il thread della GUI verrà eseguito solo mentre il thread in background sta chiamando questa funzione.

  2. Se il thread di lavoro è sufficientemente autonomo, è possibile inserirlo in un processo completamente separato e quindi comunicare inviando gli oggetti in salamoia alle pipe. Nel processo in primo piano, creare un thread di lavoro per eseguire l'IO con il processo in background. Dato che il thread worker eseguirà IO invece delle operazioni della CPU, non manterrà il GIL e questo ti darà un thread GUI completamente reattivo.

  3. Alcune implementazioni Python (JPython e IronPython) non hanno un GIL.

Discussioni nel CPython sono realmente utile solo per le operazioni di IO multiplexing, non per mettere le attività ad alta intensità di CPU in background. Per molte applicazioni, il threading nell'implementazione di CPython è fondamentalmente rotto ed è probabile che rimanga tale per il prossimo futuro.

+0

Ho provato a inserire 'time.sleep (0)' al posto del commento 'time.sleep (0.2)' nell'esempio e, sfortunatamente, non risolve il problema. Ho notato che con valori come "time.sleep (0.05)" il pulsante si comporta come previsto. Per ovviare al problema, potrei impostare l'operatore affinché riferisca l'avanzamento solo alcune volte al secondo e dorma per consentire alla GUI di aggiornarsi. – user1491306

+1

Esistono soluzioni alternative per i thread e le GUI (ad esempio http://code.activestate.com/recipes/578154). –

+0

Ho finito per fare così: metto un 'sleep' dove inizia il calcolo, e ogni volta che l'indicatore di avanzamento viene aggiornato, chiamo' app.processEvents() 'per far funzionare un pulsante" Abort ". – user1491306

0

alla fine questo funziona per il mio problema - così il codice può aiutare qualcun altro.

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_()) 
Problemi correlati