2012-06-29 67 views
11

我想在我的PySide GUI應用程序中做一個相當普遍的事情:我想委託一些CPU密集型任務到後臺線程,以便我的GUI保持隨着計算的進行,甚至可以顯示進度指示器。PySide/PyQt - 啓動一個CPU密集線程掛起整個應用程序

下面是我在做什麼(我使用PySide 1.1.1的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() 

應用程序顯示一個窗口,一個按鈕。 當按下按鈕時,我期望它在執行計算時自行禁用。然後,該按鈕應該重新啓用。

發生的情況是,當我按下按鈕時,整個窗口在計算結束時凍結,然後當結束時,我重新獲得應用程序的控制權。該按鈕從未顯示爲禁用。 我注意到一件有趣的事情,如果我用簡單的time.sleep()替換do_stuff()中的CPU密集型計算,則該程序將按預期運行。

我並不完全知道發生了什麼,但看起來第二個線程的優先級太高,實際上阻止了GUI線程被調度。如果第二個線程進入BLOCKED狀態(就像sleep()那樣),GUI實際上有機會按預期運行和更新接口。我試圖改變工作線程的優先級,但看起來它不能在Linux上完成。

此外,我嘗試打印線程ID,但我不知道如果我正確地做。如果我是,線程親和力似乎是正確的。

我也試過用PyQt程序,行爲完全一樣,因此標籤和標題。如果我可以使用PyQt4而不是PySide運行,我可以將整個應用程序切換到PyQt4

回答

12

這可能是由工作線程持有Python的GIL引起的。在一些Python實現中,一次只能執行一個Python線程。 GIL阻止其他線程執行Python代碼,並在不需要GIL的函數調用期間釋放。

例如,GIL在實際的IO期間被釋放,因爲IO由操作系統而不是Python解釋器處理。

解決方案:

  1. 顯然,你可以使用time.sleep(0)在你的工作線程來提供給其他線程(according to this SO question)。您將不得不周期性地調用time.sleep(0),並且GUI線程只在後臺線程調用此函數時運行。

  2. 如果工作線程足夠自包含,則可以將其放入完全獨立的進程,然後通過在管道上發送被醃製的對象進行通信。在前臺進程中,創建一個工作線程以使用後臺進程執行IO。由於工作線程將執行IO操作而不是CPU操作,因此它不會保存GIL,這會給你一個完全響應的GUI線程。

  3. 一些Python實現(JPython和IronPython)沒有GIL。

線程在CPython的僅供複用IO操作非常有用,而不是把CPU密集型任務的背景。對於許多應用程序而言,CPython實現中的線程基本上已經斷開,並且很可能在可預見的未來中保持這種狀態。

+0

我試着在這個例子中放置'time.sleep(0)'而不是'time.sleep(0.2)'註釋,不幸的是,並沒有解決這個問題。我注意到,像time.sleep(0.05)這樣的值,按鈕的行爲與預期相同。作爲一種解決方法,我可以設置工作人員每秒只報告幾次進度,並進行休眠以讓GUI自行更新。 – user1491306

+1

有線程和GUI的解決方法(例如http://code.activestate.com/recipes/578154)。 –

+0

我最終這樣做了:我在計算開始處放了一個'sleep',並且每次進度指示符更新時,我都會調用'app.processEvents()'以使「Abort」按鈕有效。 – user1491306

0

最後,這適用於我的問題 - 這樣的代碼可以幫助別人。

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