PyQt4: 当GUI关闭时如何中断QThread的执行

4
我有一个PyQt4 GUI,其中包含三个线程。一个线程是数据源,它提供数据的numpy数组。下一个线程是计算线程,它通过Python Queue.Queue获取numpy数组(或多个numpy数组),并计算在GUI上显示的内容。然后,计算器通过自定义信号向GUI线程(主线程)发送信号,告诉GUI更新显示的matplotlib图形。
我正在使用这里这里描述的“正确”方法。
因此,这是一般布局。我尝试缩短我的输入时间,在某些部分使用了注释而不是实际代码:
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. app.exec_()后,是否应该等待QThreads(通过thread.wait()),这是必需的吗? 4. 是否有一个由QMainWindow关闭时提供的Qt提供的信号,或者从QApplication那里? 编辑2/进展更新:我已经根据我的需求修改了这篇帖子,创建了一个可运行的问题示例。
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个回答

2
在查看邮件列表档案、谷歌搜索、堆栈溢出搜索以及思考我的问题实质和问题目的后,我得出了这个答案:
简短的答案是使用processEvents()。长答案是,所有搜索结果都表明“使用processEvents()时要非常小心”和“尽量避免使用”。如果您正在使用它,因为您在GUI主线程中没有看到足够快的结果,则应该避免使用它。在这种情况下,不用于UI目的的主线程中正在进行的工作应该移动到另一个线程中(正如我的设计所做的那样)。
我需要使用processEvents()的原因是,我希望我的QThreads与GUI线程进行双向通信,这意味着我的QThreads必须具有事件循环(exec_())以接受来自GUI的信号。这种双向通信是我之前所说的“问题目的”。由于我的QThreads旨在与主GUI线程“并发”运行,并且因为它们需要更新GUI并被GUI“更新”(例如第一个示例中的退出/关闭信号),它们需要processEvents()。我认为这就是processEvents()的用途。
如上所述,我对processEvents()的理解是,在QThread中调用它将阻塞/暂停当前事件(即我的longRunning方法),同时继续通过事件循环中的事件(仅适用于调用processEvents()的QThread)。在处理完待处理事件后,事件循环将回到原来的位置,并继续运行暂停的事件(即我的longRunning方法)。
我知道我没有回答所有问题,但主要问题已经得到了解答。
如果我有任何错误,请纠正我。
编辑:请阅读Ed的答案和评论。

2

我并没有完全阅读所有代码。我建议你不要在代码中使用循环,而是逐个运行每个逻辑块。信号/槽可以作为这些内容的透明队列。

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

是的,我使用了循环,大多数是为了举例说明,但有时你无法避免它们(比如从管道中读取)。你可以使用超时为0的QTimer或者设置一个标志来标记应该退出的事情,并用互斥量保护它。

关于编辑1: 1. 不要将exec_与长时间运行的循环混合使用 3. PySide要求您在退出线程后等待。 4. 我不记得有这样的东西,你可以将其设置为Destroy On Close,然后监视关闭,或者继承自QMainWindow,重写closeEvent并触发一个信号(就像我在qt_error_display.py示例中所做的那样)。

关于编辑2: 我建议使用默认的连接类型。

关于编辑3: 不要使用processEvents。


我理解不读所有的代码(它失控了)。问题:1.为什么不将exec_与长时间运行的循环混合使用?2.为什么不使用processEvents?它应该用于什么(我无法在任何地方找到答案)?3.我试图使用标志来标记事情应该退出,但不想锁定它。我不想锁定它,因为这是通过信号由GUI“配置”的QThread的基本示例(尽管可以用许多不同的方式完成)。我也对您构建自己的线程框架以使用PyQt4感到担忧。 - djhoese
我还应该注意到,我设计的GUI是用于实时更新数据图的。我的意思是,数据通过套接字发送,被分析,放入Python生成器中,然后传递给我的GUI代码,该代码迭代生成器并更新matplotlib图。 - djhoese
我学到的一课是:不要在线程之间使用生成器!这会导致执行流穿越线程,从而引起比你应该处理的更大的混乱。 - Ed.
尽可能保持你的代码简单,但不要过于简单。你的使用情况真的那么疯狂吗?去干扰 processEvents 吗?我想这更像是 Qt 内部用来构建每个层次的函数。正如我所说,要么使用标志向你的循环发送信号,要么使用定时器一次读取一个块,这不应该使逻辑复杂化。 - Ed.
是的,生成器在同一线程中创建和读取。我开始看到如何使用带有计时器的生成器。我只是被需要循环生成器的想法所困扰。使用间隔为0的计时器将执行我想要processEvents执行的操作(现在我明白了)。目前我没有看到使用计时器而不是processEvents存在任何问题,因此我将使用超时为0的计时器。 - djhoese
显示剩余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

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接