2012-06-29 20 views
11

Próbuję wykonać dość pospolitą rzecz w mojej aplikacji PySide GUI: Chcę przekazać trochę zadania CPU-Intensive do wątku w tle, aby mój GUI pozostał reaguje, a nawet może wyświetlać wskaźnik postępu w obliczeniach.PySide/PyQt - Uruchamianie wątku intensywnie obciążającego procesor powoduje zawieszenie się całej aplikacji

Oto, co robię (używam PySide 1.1.1 na 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() 

Aplikacja wyświetla okno z jednego przycisku. Po naciśnięciu przycisku oczekuję, że sam się wyłączy podczas wykonywania obliczeń. Następnie przycisk powinien zostać ponownie włączony.

To, co się dzieje, polega na tym, że po naciśnięciu przycisku całe okno zawiesza się, gdy obliczenia są wykonywane, a po zakończeniu odzyskuję kontrolę nad aplikacją. Przycisk nigdy nie wydaje się być wyłączony. Zabawną rzeczą, którą zauważyłem jest to, że jeśli zastąpię intensywne obliczenia procesora w do_stuff() zwykłym time.sleep(), program zachowuje się zgodnie z oczekiwaniami.

Nie wiem dokładnie, co się dzieje, ale wygląda na to, że priorytet drugiego wątku jest tak wysoki, że faktycznie uniemożliwia zaplanowanie wątku GUI. Jeśli drugi wątek przechodzi w stan ZABLOKOWANY (tak jak w przypadku sleep()), GUI ma faktycznie szansę uruchomienia i aktualizacji interfejsu zgodnie z oczekiwaniami. Próbowałem zmienić priorytet wątku roboczego, ale wygląda na to, że nie można tego zrobić w systemie Linux.

Próbuję również wydrukować identyfikatory wątków, ale nie jestem pewien, czy robię to poprawnie. Jeśli tak, powinowactwo wątku wydaje się poprawne.

Próbowałem również programu z PyQt i zachowanie jest dokładnie takie samo, stąd tagi i tytuł. Jeśli mogę uruchomić go z PyQt4 zamiast PySide, mogę zamienić całą aplikację na PyQt4

Odpowiedz

12

Jest to prawdopodobnie spowodowane wątkiem roboczym trzymającym GIL Pythona. W niektórych implementacjach Pythona można uruchamiać tylko jeden wątek Pythona naraz. GIL uniemożliwia innym wątkom wykonywanie kodu Pythona i jest zwalniany podczas wywoływania funkcji, które nie wymagają GIL.

Na przykład GIL jest zwalniany podczas rzeczywistego IO, ponieważ IO jest obsługiwane przez system operacyjny, a nie interpreter Pythona.

Solutions:

  1. Najwyraźniej można użyć time.sleep(0) w wątku roboczego w celu uzyskania do innych wątków (according to this SO question). Będziesz musiał okresowo dzwonić pod numer time.sleep(0), a wątek GUI będzie działał tylko wtedy, gdy wątek tła wywołuje tę funkcję.

  2. Jeśli wątek roboczy jest wystarczająco zamknięty, można go umieścić w całkowicie oddzielnym procesie, a następnie nawiązać połączenie, przesyłając zaszyfrowane obiekty za pomocą rur. Na pierwszym planie utwórz wątek roboczy, aby wykonać IO z procesem działającym w tle. Ponieważ wątek roboczy będzie wykonywał IO zamiast operacji CPU, nie będzie utrzymywał GIL, a to da ci całkowicie responsywny wątek GUI.

  3. Niektóre implementacje Pythona (JPython i IronPython) nie mają kodu GIL.

Tematy na CPython są tylko naprawdę użyteczne dla multipleksowania operacji IO, nie za wprowadzenie obciążających procesor zadań w tle. W przypadku wielu aplikacji wątki w implementacji CPython są zasadniczo zepsute i prawdopodobnie pozostaną w ten sposób w przewidywalnej przyszłości.

+0

Próbowałem umieścić 'time.sleep (0)' zamiast komentarza 'time.sleep (0.2)' w przykładzie i, niestety, nie rozwiązało problemu. Zauważyłem, że z wartościami takimi jak 'time.sleep (0.05)' przycisk zachowuje się zgodnie z oczekiwaniami. Aby obejść ten problem, mógłbym skonfigurować pracownika do zgłaszania postępu tylko kilka razy na sekundę i przespać się, aby zaktualizować GUI. – user1491306

+1

Istnieją obejścia dla wątków i GUI (takich jak http://code.activestate.com/recipes/578154). –

+0

Skończyłem w ten sposób: wstawiłem "spać", gdzie zaczyna się obliczanie, i za każdym razem, gdy wskaźnik postępu jest aktualizowany, wołam 'app.processEvents()', aby zadziałał przycisk "Przerwij". – user1491306

0

na końcu to działa na mój problem - niech kod pomoże komuś innemu.

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