PyQt4:GUI关闭时中断QThread exec

时间:2011-08-18 02:03:47

标签: python qt4 pyqt pyqt4

我有一个有三个线程的PyQt4 GUI。一个线程是一个数据源,它提供了数据的numpy数组。下一个线程是计算线程,它通过Python Queue.Queue获取numpy数组(或多个numpy数组)并计算将在GUI上显示的内容。然后计算器通过自定义信号发出GUI线程(主线程)的信号,这告诉GUI更新显示的matplotlib图形。

我正在使用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后,当calc线程发出更新gui信号(BlockedQueuedConnection信号)时,它似乎挂起。我猜这是因为GUI已经关闭,不能接受发出的信号(根据我在实际代码中输入的打印消息判断)。

我一直在查看大量的教程和文档,我觉得我做的事情很愚蠢。这是可能的,有一个事件循环和一个“无限”的运行循环,它提前结束......并且安全地(资源正确关闭)?

我也对我的BlockedQueuedConnection问题感到好奇(如果我的描述有意义),但是这个问题可能通过我没有看到的简单重新设计来解决。

感谢您的帮助,让我知道什么是没有意义的。如果需要的话,我也可以在代码中添加更多内容而不仅仅是做评论(我有点希望我做了一些愚蠢的事情并且不需要它。)

编辑:我找到了一些解决方法,但是,我认为我很幸运它到目前为止每次都有效。如果我使用prepare_exit和thread.quit连接DirectConnections,它会在主线程中运行函数调用,并且程序不会挂起。

我还想我应该总结一些问题:

  1. QThread可以有一个事件循环(通过exec_)并且有一个长时间运行的循环吗?
  2. 如果接收器断开插槽(在发出信号之后,但在确认之前),BlockingQueuedConnection发射器是否会挂起?
  3. 我应该在app.exec_()之后等待QThreads(通过thread.wait())吗?
  4. QMainWindow关闭时是否有Qt提供的信号,或QApplication中是否存在?
  5. 编辑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时它会导致调用者的事件循环停止其当前操作并继续处理事件循环中的挂起事件,最终继续长计算事件。像this email中的其他建议建议计时器或将工作放在其他线程中,我认为这只会使我的工作变得更加复杂,特别是因为我已经证明(我认为)计时器在我的情况下不起作用。如果processEvents似乎解决了我的所有问题,我将在稍后回答我自己的问题。

4 个答案:

答案 0 :(得分:2)

浏览邮件列表档案,谷歌搜索,堆栈溢出搜索,并思考我的问题到底是什么,问题的目的是什么,我想出了这个答案:

简短的回答是使用processEvents()。答案很长,我所有的搜索都会导致人们说“使用processEvents()时要非常小心”并“不惜一切代价避免它”。我认为如果你使用它应该避免,因为你没有在GUI主线程中看到足够快的结果。在这种情况下,不是在使用processEvents,而是在非主题用途的主线程中完成的工作应该移动到另一个线程(正如我的设计所做的那样)。

我的特定问题需要processEvents()的原因是我希望我的QThreads与GUI线程进行双向通信,这意味着我的QThreads必须有一个事件循环(exec_())来接受来自GUI的信号。这种双向沟通就是我之前提到的“问题的目的”。由于我的QThreads意图与主GUI线程“并发”运行,因为它们需要更新GUI并由GUI“更新”(我的第一个例子中的退出/关闭信号),它们需要processEvents()。我认为这就是processEvents()的用途。

我对processEvents()的理解,如上所述,当在QThread中调用时,它将阻塞/暂停当前事件(我的longRunning方法),同时继续通过事件循环中的事件(仅针对QThread) processEvents()被调用)。在经历挂起事件之后,事件循环回绕并继续运行它暂停的事件(我的longRunning方法)。

我知道我没有回答所有问题,但主要问题已得到解答。

如果我以任何方式错误,请纠正我

编辑:请阅读Ed的回答和评论。

答案 1 :(得分:2)

老实说,我没有阅读所有代码。我建议不要在代码中使用循环,而是一次运行每个逻辑块。信号/插槽也可以作为这些事物的透明队列。

我写过的一些生产者/消费者示例代码 https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py 我编写了一些不同的线程代码和更高级的工具 https://github.com/epage/PythonUtils/blob/master/qt_error_display.py

是的我使用循环,主要是出于示例目的,但有时你无法避免它们(比如从管道中读取)。您可以使用QTimer,超时为0,或者使用一个标记来标记应该退出并使用互斥锁保护它。

RE EDIT 1: 1.不要将exec_与长时间运行的循环混合使用 3. PySide要求你在退出线程后等待。 4.我不记得有一个,你可以将它设置为Destroy On Close然后监视关闭或者你可以从QMainWindow继承,覆盖closeEvent并触发一个信号(就像我在qt_error_display.py例子中那样)

RE EDIT 2: 我建议使用默认的连接类型。

RE EDIT 3:不要使用processEvents。

答案 2 :(得分:0)

您可以将工作负载分成多个块,并按以下建议在单独的插槽调用中一一处理:https://wiki.qt.io/Threads_Events_QObjects

import time
import sys

from PyQt5 import QtCore
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QMetaObject, Qt, QThread


class intObject(QtCore.QObject):
    finished = pyqtSignal()
    interrupt_signal = pyqtSignal()

    def __init__(self):
        QtCore.QObject.__init__(self)
        print("__init__ of interrupt Thread: %d" % QThread.currentThreadId())
        QtCore.QTimer.singleShot(3000, self.send_interrupt)

    @pyqtSlot()
    def send_interrupt(self):
        print("send_interrupt Thread: %d" % QThread.currentThreadId())
        self.interrupt_signal.emit()
        self.finished.emit()

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

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

    @pyqtSlot()
    def longRunning(self):
        if self.count == 0:
            print("longrunning Thread: %d" % QThread.currentThreadId())
            print("Running longrunning")
        if self._exiting:
            print('premature exit')
            self.finished.emit()
        elif self.count < 5:
            print(self.count, 'sleeping')
            time.sleep(2)
            print(self.count, 'awoken')
            self.count += 1
            QMetaObject.invokeMethod(self, 'longRunning',  Qt.QueuedConnection)
        else:
            print('normal exit')
            self.finished.emit()


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

def usingMoveToThread():
    app = QtCore.QCoreApplication([])
    print("Main Thread: %d" % 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=Qt.DirectConnection)
    intobj.interrupt_signal.connect(obj.interrupt, type=Qt.QueuedConnection)

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

if __name__ == "__main__":
    usingMoveToThread()

结果:

Main Thread: 19940
__init__ of interrupt Thread: 19940
__init__ of obj Thread: 19940
longrunning Thread: 18040
Running longrunning
0 sleeping
0 awoken
1 sleeping
send_interrupt Thread: 7876
1 awoken
Running interrupt
interrupt Thread: 18040
premature exit

答案 3 :(得分:0)

代替QMetaObject.invokeMethod,也可以使用QTimer,超时为0,如此处建议的那样:https://doc.qt.io/qt-5/qtimer.html