2012-06-29 8 views
11

Ich versuche, eine ziemlich häufige Sache in meiner PySide GUI-Anwendung zu tun: Ich möchte einige CPU-intensive Aufgabe an einen Hintergrund Thread delegieren, so dass meine GUI bleibt reagieren und könnte sogar eine Fortschrittsanzeige anzeigen, wie die Berechnung geht.PySide/PyQt - Starten eines CPU-intensiven Threads hängt die ganze Anwendung

Hier ist, was ich tue (ich benutze pyside 1.1.1 auf 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() 

Die Anwendung zeigt ein einzelnes Fenster mit einem Button. Wenn die Taste gedrückt wird, erwarte ich, dass sie sich selbst deaktiviert, während die Berechnung durchgeführt wird. Dann sollte die Schaltfläche wieder aktiviert werden.

Was passiert, ist stattdessen, dass, wenn ich den Knopf drücke das ganze Fenster einfriert, während die Berechnung geht und dann, wenn es fertig ist, ich wieder die Kontrolle über die Anwendung. Die Schaltfläche scheint nie deaktiviert zu sein. Eine lustige Sache, die ich bemerkte, ist, dass, wenn ich die CPU-intensive Berechnung in do_stuff() mit einem einfachen time.sleep() ersetze, das Programm sich wie erwartet verhält.

Ich weiß nicht genau, was los ist, aber es scheint, dass die Priorität des zweiten Threads so hoch ist, dass es tatsächlich verhindert, dass der GUI-Thread überhaupt geplant wird. Wenn der zweite Thread in den BLOCKED-Zustand wechselt (wie es bei einem sleep() geschieht), hat die GUI tatsächlich die Möglichkeit zu laufen und aktualisiert die Schnittstelle wie erwartet. Ich habe versucht, die Worker Thread-Priorität zu ändern, aber es sieht so aus, als ob es unter Linux nicht möglich ist.

Auch ich versuche, die Thread-IDs zu drucken, aber ich bin mir nicht sicher, ob ich es richtig mache. Wenn ich bin, scheint die Threadaffinität korrekt zu sein.

Ich habe auch versucht das Programm mit PyQt und das Verhalten ist genau das gleiche, daher die Tags und Titel. Wenn ich es mit PyQt4 statt PySide laufen lassen kann, könnte ich meine ganze Anwendung zu PyQt4 wechseln

Antwort

12

Dies wird wahrscheinlich durch den Worker-Thread verursacht, der Pythons GIL hält. In einigen Python-Implementierungen kann jeweils nur ein Python-Thread ausgeführt werden. Die GIL verhindert, dass andere Threads Python-Code ausführen, und wird während Funktionsaufrufen freigegeben, die die GIL nicht benötigen.

Zum Beispiel wird die GIL während der aktuellen IO freigegeben, da IO vom Betriebssystem und nicht vom Python-Interpreter verarbeitet wird.

Lösungen:

  1. Offenbar können Sie time.sleep(0) in Ihrem Arbeitsthread verwenden, um andere Threads (according to this SO question) zu erhalten. Sie müssen in regelmäßigen Abständen selbst time.sleep(0) aufrufen, und der GUI-Thread wird nur ausgeführt, während der Hintergrundthread diese Funktion aufruft.

  2. Wenn der Worker-Thread ausreichend eigenständig ist, können Sie ihn in einen vollständig separaten Prozess einfügen und dann kommunizieren, indem Sie gebeizte Objekte über Pipes senden. Erstellen Sie im Vordergrundprozess einen Worker-Thread, um IO mit dem Hintergrundprozess auszuführen. Da der Worker-Thread IO statt CPU-Operationen ausführt, wird er nicht die GIL enthalten, und dies wird Ihnen einen vollständig ansprechenden GUI-Thread geben.

  3. Einige Python-Implementierungen (JPython und IronPython) haben kein GIL.

Themen in CPython sind nur dann wirklich nützlich für die Multiplexing-IO-Operationen, nicht für die Umsetzung CPU-intensiver Aufgaben im Hintergrund. Für viele Anwendungen ist das Threading in der CPython-Implementierung grundlegend gebrochen und es wird wahrscheinlich so bleiben für die absehbare Zukunft.

+0

Ich habe versucht, 'time.sleep (0)' anstatt des 'time.sleep (0.2)' Kommentar im Beispiel zu setzen und, leider, behebt das Problem nicht. Ich habe festgestellt, dass sich die Schaltfläche bei Werten wie "time.sleep (0.05)" wie erwartet verhält. Als Workaround konnte ich den Worker so einrichten, dass der Fortschritt nur einige Male pro Sekunde gemeldet wird, und der Ruhezustand, damit sich die GUI selbst aktualisieren kann. – user1491306

+1

Es gibt Problemumgehungen für Threads und GUIs (z. B. http://code.activestate.com/recipes/578154). –

+0

Ich endete so: Ich legte einen "Schlaf", wo die Berechnung beginnt, und jedes Mal, wenn die Fortschrittsanzeige aktualisiert wird, rufe ich 'app.processEvents()' auf, um einen "Abbrechen" -Button zu aktivieren. – user1491306

0

am Ende funktioniert das für mein Problem - so kann der Code jemand anderem helfen.

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