2011-08-18 213 views
3

我有一個有三個線程的PyQt4 GUI。一個線程是一個數據源,它提供了數據的numpy數組。下一個線程是一個計算線程,它通過Python Queue.Queue獲取numpy數組(或多個numpy數組),並計算將在GUI上顯示的內容。然後計算器通過一個自定義信號向GUI線程(主線程)發出信號,這告訴GUI更新顯示的matplotlib圖形。PyQt4:當GUI關閉時中斷QThread exec

我使用了「正確」的方法中所描述herehere

所以這裏的總體佈局。我試圖縮短我的打字時間和使用的意見,而不是在一些地方的實際代碼:

class Source(QtCore.QObject): 
    signal_finished = pyQtSignal(...) 
    def __init__(self, window): 
     self._exiting = False 
     self._window = window 

    def do_stuff(self): 
     # Start complicated data generator 
     for data in generator: 
      if not self._exiting: 
       # Get data from generator 
       # Do stuff - add data to Queue 
       # Loop ends when generator ends 
      else: 
       break 
     # Close complicated data generator 

    def prepare_exit(self): 
     self._exiting = True 

class Calculator(QtCore.QObject): 
    signal_finished = pyQtSignal(...) 
    def __init__(self, window): 
     self._exiting = False 
     self._window = window 

    def do_stuff(self): 
     while not self._exiting: 
      # Get stuff from Queue (with timeout) 
      # Calculate stuff 
      # Emit signal to GUI 
      self._window.signal_for_updating.emit(...) 

    def prepare_exit(self): 
     self._exiting = True 

class GUI(QtCore.QMainWindow): 
    signal_for_updating = pyQtSignal(...) 
    signal_closing = pyQtSignal(...) 
    def __init__(self): 
     self.signal_for_updating.connect(self.update_handler, type=QtCore.Qt.BlockingQueuedConnection) 
    # Other normal GUI stuff 
    def update_handler(self, ...): 
     # Update GUI 
    def closeEvent(self, ce): 
     self.fileQuit() 
    def fileQuit(self): # Used by a menu I have File->Quit 
     self.signal_closing.emit() # Is there a builtin signal for this 

if __name__ == '__main__': 
    app = QtCore.QApplication([]) 
    gui = GUI() 
    gui.show() 

    source_thread = QtCore.QThread() # This assumes that run() defaults to calling exec_() 
    source = Source(window) 
    source.moveToThread(source_thread) 

    calc_thread = QtCore.QThread() 
    calc = Calculator(window) 
    calc.moveToThread(calc_thread) 

    gui.signal_closing.connect(source.prepare_exit) 
    gui.signal_closing.connect(calc.prepare_exit) 
    source_thread.started.connect(source.do_stuff) 
    calc_thread.started.connect(calc.do_stuff) 
    source.signal_finished.connect(source_thread.quit) 
    calc.signal_finished.connect(calc_thread.quit) 

    source_thread.start() 
    calc_thread.start() 
    app.exec_() 
    source_thread.wait() # Should I do this? 
    calc_thread.wait() # Should I do this? 

...所以,我的所有問題,當我試圖關閉GUI源完成之前發生的,當我讓數據生成器完成它關閉罰款:

  • 在等待線程時,程序掛起。據我所知,這是因爲閉合信號的連接插槽永遠不會被其他線程的事件循環運行(它們被困在「無限」運行的do_stuff方法中)。

  • 當計算線程發出GUI結束後右的更新GUI信號(BlockedQueuedConnection信號),它似乎掛起。我猜這是因爲GUI已經關閉,不能接受發射的信號(根據我在實際代碼中輸入的打印信息判斷)。

我一直在瀏覽大量的教程和文檔,我只是覺得我在做一些愚蠢的事情。這是可能的,有一個事件循環和一個「無限」的運行循環結束早期......並安全地(資源正確關閉)?

我也好奇我BlockedQueuedConnection的問題(如果我的描述是有道理的),但這個問題可能是與我沒有看到一個簡單的重新設計可以解決的。

感謝您的幫助,讓我知道什麼是沒有意義的。如果需要的話,我也可以添加更多的代碼,而不是僅僅做評論(我有點希望我做了一些愚蠢的事情,而不需要)。

編輯:我發現了一些周圍的工作是什麼,但是,我想我只是幸運,它屢試不爽至今。如果我使用prepare_exit和thread.quit連接DirectConnections,它將在主線程中運行函數調用,並且程序不會掛起。

我也想我應該總結的一些問題:

  1. 一個的QThread能有一個事件循環(通過exec_),並有一個長期運行的循環?
  2. 是否一個BlockingQueuedConnection發射掛起如果接收機斷開槽(信號被髮射之後,但在此之前人們承認)?
  3. 我應該等待QThreads(通過thread.wait())app.exec_後(),這是需要的?
  4. 是否有一個Qt爲QMainWindow的關閉時提供的信號,或者是有一個從所述的QApplication?

編輯2 /進度情況:我已經適應this post我的需要造成的問題的一個可運行的例子。

from PyQt4 import QtCore 
import time 
import sys 


class intObject(QtCore.QObject): 
    finished = QtCore.pyqtSignal() 
    interrupt_signal = QtCore.pyqtSignal() 
    def __init__(self): 
     QtCore.QObject.__init__(self) 
     print "__init__ of interrupt Thread: %d" % QtCore.QThread.currentThreadId() 
     QtCore.QTimer.singleShot(4000, self.send_interrupt) 
    def send_interrupt(self): 
     print "send_interrupt Thread: %d" % QtCore.QThread.currentThreadId() 
     self.interrupt_signal.emit() 
     self.finished.emit() 

class SomeObject(QtCore.QObject): 
    finished = QtCore.pyqtSignal() 
    def __init__(self): 
     QtCore.QObject.__init__(self) 
     print "__init__ of obj Thread: %d" % QtCore.QThread.currentThreadId() 
     self._exiting = False 

    def interrupt(self): 
     print "Running interrupt" 
     print "interrupt Thread: %d" % QtCore.QThread.currentThreadId() 
     self._exiting = True 

    def longRunning(self): 
     print "longRunning Thread: %d" % QtCore.QThread.currentThreadId() 
     print "Running longRunning" 
     count = 0 
     while count < 5 and not self._exiting: 
      time.sleep(2) 
      print "Increasing" 
      count += 1 

     if self._exiting: 
      print "The interrupt ran before longRunning was done" 
     self.finished.emit() 

class MyThread(QtCore.QThread): 
    def run(self): 
     self.exec_() 

def usingMoveToThread(): 
    app = QtCore.QCoreApplication([]) 
    print "Main Thread: %d" % QtCore.QThread.currentThreadId() 

    # Simulates user closing the QMainWindow 
    intobjThread = MyThread() 
    intobj = intObject() 
    intobj.moveToThread(intobjThread) 

    # Simulates a data source thread 
    objThread = MyThread() 
    obj = SomeObject() 
    obj.moveToThread(objThread) 

    obj.finished.connect(objThread.quit) 
    intobj.finished.connect(intobjThread.quit) 
    objThread.started.connect(obj.longRunning) 
    objThread.finished.connect(app.exit) 
    #intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.DirectConnection) 
    intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.QueuedConnection) 

    objThread.start() 
    intobjThread.start() 
    sys.exit(app.exec_()) 

if __name__ == "__main__": 
    usingMoveToThread() 

您可以通過運行該代碼,並在兩種連接類型之間的交換上interrupt_signal,直接連接工作看,因爲它在一個單獨的線程運行,正確或錯誤的做法?我覺得這是不好的做法,因爲我正在快速改變另一個線程正在閱讀的內容。該QueuedConnection不起作用,因爲事件循環必須等待,直到事件循環獲取前回繞到中斷信號,這是不是我想要的longRunning完成。

編輯3:我記得讀取QtCore.QCoreApplication.processEvents可以與長時間運行的計算的情況下使用,但我讀到的一切說除非你知道自己在做什麼,不使用它。那麼這裏是什麼,我認爲它在做什麼(在一定意義上),並使用它似乎工作:當你打電話processEvents它會導致調用者的事件循環hault其當前操作,並繼續處理事件循環的未決事件,最終繼續長計算事件。像this email其他建議建議定時器或投入其他線程的工作,我認爲這只是讓我的工作更加複雜,尤其是因爲我已經證明了(我認爲)計時器沒有在我的情況下工作。如果processEvents似乎解決了我所有的問題,我會在稍後回答我自己的問題。

回答

2

我誠實地沒有讀取所有的代碼。我建議不要在代碼中出現循環,而是一次運行每個邏輯塊。信號/插槽也可以作爲這些東西的透明隊列。

我已經寫了一些生產者/消費者的示例代碼 https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py 更先進的utils的一些不同的線程代碼,我已經寫了 https://github.com/epage/PythonUtils/blob/master/qt_error_display.py

是的,我使用的環路,主要用於舉例的目的,但有時你不能避免它們(如從管道讀取)。您可以使用QTimer的超時時間爲0或有一個標誌來標記事情應該退出並用互斥鎖保護它。

RE編輯1: 1.不要將exec_與長時間運行的循環混合使用 3. PySide要求您在退出線程後等待。 4.我不記得有一個,你可以將它設置爲Destroy On Close,然後監視關閉,或者你可以從QMainWindow繼承,覆蓋closeEvent併發出一個信號(就像我在qt_error_display.py示例中那樣)

RE編輯2: 我建議使用默認的連接類型。

RE編輯3:不要使用processEvents。

+0

我不明白所有的代碼(它失控了)。 Questons:1.爲什麼不把exec_與長時間運行的循環混合? 2.爲什麼不使用processEvents?它應該用於什麼(我永遠無法在任何地方找到這個答案)? 3.我試圖用一面旗幟標記事情應該放棄,但不想鎖定它。我不想鎖定它,因爲這是QThread由GUI通過信號「配置」的一個基本示例(儘管這可以通過許多不同的方式完成)。我也厭倦了你構建自己的線程框架來使用PyQt4的事實。 – daveydave400

+0

我也應該注意到我設計的GUI是用於實時更新數據圖。我的意思是通過套接字發送數據,並將其分析並放入一個python生成器,該生成器傳遞給我的GUI代碼,該代碼遍歷生成器並更新matplotlib圖。 – daveydave400

+0

我學到的一課:不要在線程間使用生成器!這導致執行流程交叉線程,導致更大的混亂,然後你應該處理。 –

2

通過郵件列表歸檔看後,谷歌搜索,堆棧溢出搜索和思考什麼,我的問題真的是什麼這個問題的目的是我想出了這樣的回答:

簡短的回答是使用processEvents()。漫長的回答是,我所有的搜索結果都會讓人們「使用processEvents()非常小心」,並且「不惜一切代價避免它」。我認爲應該避免使用它,因爲在GUI主線程中看不到結果足夠快。在這種情況下,不是使用processEvents,而是在主線程中完成的非UI用途的工作應該移動到另一個線程(如我的設計所做的那樣)。

我的具體問題需要processEvents()的原因是我希望我的QThreads與GUI線程有雙向通信,這意味着我的QThreads必須有一個事件循環(exec_())來接受來自GUI。這種雙向交流就是我之前所說的「問題的目的」。由於我的QThreads是爲了運行「同時」與主界面線程因爲他們需要更新的GUI和圖形用戶界面(在我的第一個例子退出/關閉信號)進行「更新」,他們需要processEvents()。我認爲這是processEvents()的用途。

我對processEvents()的理解,如上所述,當在QThread中調用時,它將阻止/暫停當前事件(我的longRunning方法),同時繼續處理事件循環中的事件(僅限於QThread processEvents()被調用)。在經歷未決事件之後,事件循環迴繞並繼續運行暫停的事件(我的longRunning方法)。

我知道我沒有回答我所有的問題,但主要的一個回答。

請糾正我,如果我錯了以任何方式

編輯:請閱讀Ed的回答和評論。