PyQt4:关闭GUI时中断QThread exec。

时间:2021-07-26 22:58:49

I have a PyQt4 GUI that has three threads. One thread is a data source, it provides numpy arrays of data. The next thread is a calculation thread, it takes the numpy array (or multiple numpy arrays) via a Python Queue.Queue and calculates what will be displayed on the GUI. The calculator then signals the GUI thread (the main thread) via a custom signal and this tells the GUI to update the matplotlib figure that's displayed.

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

I'm using the "proper" method described here and here.

我正在使用这里和这里描述的“适当”方法。

So here's the general layout. I tried to shorten my typing time and used comments instead of the actual code in some parts:

这是总体布局。我试着缩短打字时间,用注释代替了一些部分的实际代码:

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?

...So, my problems all occur when I try to close the GUI before the sources are complete, when I let the data generators finish it closes fine:

…所以,我的问题都发生在我试图在源完成之前关闭GUI的时候,当我让数据生成器完成时,它关闭的很好:

  • While waiting for the threads, the program hangs. As far as I can tell this is because the closing signal's connected slots never get run by the other thread's event loops (they're stuck on the "infinitely" running do_stuff method).

    在等待线程时,程序挂起了。据我所知,这是因为关闭信号的连接槽永远不会被其他线程的事件循环运行(它们被困在“无限”运行的do_stuff方法中)。

  • When the calc thread emits the updating gui signal (a BlockedQueuedConnection signal) right after the GUI closing, it seems to hang. I'm guessing this is because the GUI is already closed and isn't there to accept the emitted signal (judging by the print messages I put in my actual code).

    当calc线程在gui关闭后发出更新的gui信号(一个BlockedQueuedConnection信号)时,它似乎挂起了。我猜这是因为GUI已经关闭了,并没有接受发出的信号(从我在实际代码中输入的打印消息来判断)。

I've been looking through tons of tutorials and documentation and I just feel like I'm doing something stupid. Is this possible, to have an event loop and an "infinite" running loop that end early...and safely (resources closed properly)?

我一直在浏览大量的教程和文档,我只是觉得我在做一些愚蠢的事情。这是否可能,有一个事件循环和一个“无限”运行循环结束的早期…安全(资源适当关闭)?

I'm also curious about my BlockedQueuedConnection problem (if my description makes sense), however this problem is probably fixable with a simple redesign that I'm not seeing.

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

Thanks for any help, let me know what doesn't make sense. If it's needed I can also add more to the code instead of just doing comments (I was kind of hoping that I did something dumb and it wouldn't be needed).

谢谢你的帮助,让我知道什么是没有意义的。如果需要,我还可以在代码中添加更多内容,而不是只做注释(我希望我做了一些愚蠢的事情,这是不需要的)。

Edit: I found some what of a work around, however, I think I'm just lucky that it works every time so far. If I make the prepare_exit and the thread.quit connections DirectConnections, it runs the function calls in the main thread and the program does not hang.

编辑:我找到了一些关于工作的东西,但是,我觉得我很幸运,它每次都能起作用。如果我做prepare_exit和线程。退出连接DirectConnections,它在主线程中运行函数调用,程序不挂起。

I also figured I should summarize some questions:

我还认为我应该总结一些问题:

  1. Can a QThread have an event loop (via exec_) and have a long running loop?
  2. QThread有一个事件循环(通过exec_)并有一个长运行循环吗?
  3. Does a BlockingQueuedConnection emitter hang if the receiver disconnects the slot (after the signal was emitted, but before it was acknowledged)?
  4. 如果接收器断开了插槽(在信号发出后,但在被确认之前),阻塞的queuedconnection发射器会挂起吗?
  5. Should I wait for the QThreads (via thread.wait()) after app.exec_(), is this needed?
  6. 我是否应该等待QThreads(通过线程.wait())在app.exec_()之后,这是否需要?
  7. Is there a Qt provided signal for when QMainWindow closes, or is there one from the QApplication?
  8. 当QMainWindow关闭时,是否有Qt提供的信号,或者是否有来自QApplication的信号?

Edit 2/Update on progress: I have created a runnable example of the problem by adapting this post to my needs.

编辑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()

You can see by running this code and swapping between the two connection types on interrupt_signal that the direct connection works because its running in a separate thread, proper or bad practice? I feel like that is bad practice because I am quickly changing something that another thread is reading. The QueuedConnection does not work because the event loop must wait until longRunning is finished before the event loop gets back around to the interrupt signal, which is not what I want.

通过运行这个代码并在interrupt_signal上的两个连接类型之间进行交换,您可以看到直接连接的工作方式是在单独的线程中运行,是正确的还是错误的?我觉得这是很糟糕的练习,因为我正在快速改变另一个线程正在阅读的东西。QueuedConnection不起作用,因为事件循环必须等待,直到在事件循环返回到中断信号之前完成longRunning,这不是我想要的。

Edit 3: I remembered reading that QtCore.QCoreApplication.processEvents can be used in cases with long running calculations, but everything I read said don't use it unless you know what you are doing. Well here is what I think it's doing (in a sense) and using it seems to work: When you call processEvents it causes the caller's event loop to hault its current operation and continue on processing the pending events in the event loop, eventually continuing the long calculation event. Other recommendations like in this email suggest timers or putting the work in other threads, I think this just makes my job even more complicated, especially since I've proven(I think) timers don't work in my case. If processEvents seems to fix all my problems I will answer my own question later.

编辑3:我记得读过QtCore.QCoreApplication。processEvents可以用于长时间运行的计算,但是我读到的所有东西都说不要使用它,除非你知道你在做什么。这里是我认为它正在做的事情(在某种意义上),并且使用它似乎是有效的:当您调用processEvents时,它会导致调用者的事件循环到当前的操作,并继续处理事件循环中的挂起事件,最终继续进行长计算事件。其他的建议比如在这封邮件中建议定时器或者把工作放到其他的线程中,我认为这只会让我的工作更加复杂,特别是因为我已经证明了(我认为)计时器对我的情况不起作用。如果processEvents似乎解决了我所有的问题,我以后会回答我自己的问题。

2 个解决方案

#1


2  

I honestly did not read all of the code. I would recommend against having loops in your code but instead run each logical chunk at a time. Signals/Slots can work as transparent queues for these things too.

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

Some producer/consumer example code I've written https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py Some different threading code with more advanced utils I've written https://github.com/epage/PythonUtils/blob/master/qt_error_display.py

我已经编写了一些生产者/消费者的示例代码,其中包括https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py。

Yes I used loops, mostly for example purposes but sometimes you can't avoid them (like reading from an pipe). You can either use QTimer with a timeout of 0 or have a flag to mark that things should quit and protect it with a mutex.

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

RE EDIT 1: 1. Don't mix exec_ with long running loops 3. PySide requires that you wait after quitting a thread. 4. I don't remember there being one, you can set it to Destroy On Close and then monitor for close or you can inherit from QMainWindow, override closeEvent and fire a signal (like I do in the qt_error_display.py example)

重新编辑1:1。不要将exec_与长运行的循环3混合。PySide要求您在退出线程后等待。4所示。我不记得有一个,你可以把它设置为关闭,然后监控关闭或者你可以从QMainWindow继承,重写关闭事件并触发一个信号(就像我在qt_error_display中做的那样)。py例子)

RE EDIT 2: I'd recommend using the default connection types.

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

RE EDIT 3: Don't use processEvents.

重新编辑3:不要使用processEvents。

#2


2  

After looking through the mailing list archives, google searching, stack overflow searching, and thinking about what my question really was and what the purpose of the question was I came up with this answer:

在查看邮件列表档案,谷歌搜索,stack overflow搜索,思考我的问题到底是什么以及问题的目的是什么我找到了这个答案:

The short answer being use processEvents(). The long answer is that all my searching results in people saying "be very careful using processEvents()" and "avoid it at all costs". I think it should be avoided if you are using it because you are not seeing results in your GUI main thread fast enough. Instead of using processEvents in this case, the work being done in the main thread that is not UI purposed should be moved to another thread (as my design has done).

简短的回答是使用processEvents()。长期的回答是,我所有的搜索结果都是在人们说“小心使用processEvents()”和“不惜一切代价避免它”。我认为如果您使用它,应该避免它,因为您没有看到您的GUI主线程的结果足够快。在这种情况下,不是使用processEvents,而是在主线程中完成的工作,而不是UI目的,应该转移到另一个线程(正如我的设计所做的那样)。

The reason my specific problem needs processEvents() is that I want my QThreads to have two way communication with the GUI thread, which means that my QThreads have to have an event loop (exec_()) to accept signals from the GUI. This two way communication is what I meant earlier by "the purpose of the question". Since my QThreads are meant to run "concurrently" with the main GUI thread AND because they need to update the GUI and be "updated" by the GUI (the exit/closing signal in my first example), they need processEvents(). I think this is what processEvents() is for.

我的具体问题需要processEvents()的原因是,我希望我的QThreads与GUI线程有双向通信,这意味着我的QThreads必须有一个事件循环(exec_())来接受来自GUI的信号。这两种沟通方式是我之前所说的“问题的目的”。因为我的QThreads应该与主GUI线程同时运行,并且因为它们需要更新GUI,并被GUI(我的第一个示例中的退出/关闭信号)“更新”,所以它们需要processEvents()。我认为这就是processEvents()的目的。

My understanding of processEvents(), as decribed above, is that when called in a QThread it will block/pause the current event (my longRunning method) while it continues on through the events in the event loop (only for the QThread processEvents() was called in). After going through the pending events, the event loop wraps back around and continues running the event that it paused (my longRunning method).

我对processEvents()的理解,就像上面的decribed一样,当在一个QThread中调用时,它会阻塞/暂停当前事件(我的longRunning方法),而它会继续在事件循环中的事件中(只在QThread processEvents()中调用)。在完成挂起事件之后,事件循环将返回并继续运行它暂停的事件(我的longRunning方法)。

I know I didn't answer all my questions, but the main one is answered.

我知道我没有回答我所有的问题,但主要的回答是。

PLEASE CORRECT ME IF I AM WRONG IN ANY WAY

如果我错了,请纠正我。

Edit: Please read Ed's answer and the comments.

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

#1


2  

I honestly did not read all of the code. I would recommend against having loops in your code but instead run each logical chunk at a time. Signals/Slots can work as transparent queues for these things too.

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

Some producer/consumer example code I've written https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py Some different threading code with more advanced utils I've written https://github.com/epage/PythonUtils/blob/master/qt_error_display.py

我已经编写了一些生产者/消费者的示例代码,其中包括https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py。

Yes I used loops, mostly for example purposes but sometimes you can't avoid them (like reading from an pipe). You can either use QTimer with a timeout of 0 or have a flag to mark that things should quit and protect it with a mutex.

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

RE EDIT 1: 1. Don't mix exec_ with long running loops 3. PySide requires that you wait after quitting a thread. 4. I don't remember there being one, you can set it to Destroy On Close and then monitor for close or you can inherit from QMainWindow, override closeEvent and fire a signal (like I do in the qt_error_display.py example)

重新编辑1:1。不要将exec_与长运行的循环3混合。PySide要求您在退出线程后等待。4所示。我不记得有一个,你可以把它设置为关闭,然后监控关闭或者你可以从QMainWindow继承,重写关闭事件并触发一个信号(就像我在qt_error_display中做的那样)。py例子)

RE EDIT 2: I'd recommend using the default connection types.

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

RE EDIT 3: Don't use processEvents.

重新编辑3:不要使用processEvents。

#2


2  

After looking through the mailing list archives, google searching, stack overflow searching, and thinking about what my question really was and what the purpose of the question was I came up with this answer:

在查看邮件列表档案,谷歌搜索,stack overflow搜索,思考我的问题到底是什么以及问题的目的是什么我找到了这个答案:

The short answer being use processEvents(). The long answer is that all my searching results in people saying "be very careful using processEvents()" and "avoid it at all costs". I think it should be avoided if you are using it because you are not seeing results in your GUI main thread fast enough. Instead of using processEvents in this case, the work being done in the main thread that is not UI purposed should be moved to another thread (as my design has done).

简短的回答是使用processEvents()。长期的回答是,我所有的搜索结果都是在人们说“小心使用processEvents()”和“不惜一切代价避免它”。我认为如果您使用它,应该避免它,因为您没有看到您的GUI主线程的结果足够快。在这种情况下,不是使用processEvents,而是在主线程中完成的工作,而不是UI目的,应该转移到另一个线程(正如我的设计所做的那样)。

The reason my specific problem needs processEvents() is that I want my QThreads to have two way communication with the GUI thread, which means that my QThreads have to have an event loop (exec_()) to accept signals from the GUI. This two way communication is what I meant earlier by "the purpose of the question". Since my QThreads are meant to run "concurrently" with the main GUI thread AND because they need to update the GUI and be "updated" by the GUI (the exit/closing signal in my first example), they need processEvents(). I think this is what processEvents() is for.

我的具体问题需要processEvents()的原因是,我希望我的QThreads与GUI线程有双向通信,这意味着我的QThreads必须有一个事件循环(exec_())来接受来自GUI的信号。这两种沟通方式是我之前所说的“问题的目的”。因为我的QThreads应该与主GUI线程同时运行,并且因为它们需要更新GUI,并被GUI(我的第一个示例中的退出/关闭信号)“更新”,所以它们需要processEvents()。我认为这就是processEvents()的目的。

My understanding of processEvents(), as decribed above, is that when called in a QThread it will block/pause the current event (my longRunning method) while it continues on through the events in the event loop (only for the QThread processEvents() was called in). After going through the pending events, the event loop wraps back around and continues running the event that it paused (my longRunning method).

我对processEvents()的理解,就像上面的decribed一样,当在一个QThread中调用时,它会阻塞/暂停当前事件(我的longRunning方法),而它会继续在事件循环中的事件中(只在QThread processEvents()中调用)。在完成挂起事件之后,事件循环将返回并继续运行它暂停的事件(我的longRunning方法)。

I know I didn't answer all my questions, but the main one is answered.

我知道我没有回答我所有的问题,但主要的回答是。

PLEASE CORRECT ME IF I AM WRONG IN ANY WAY

如果我错了,请纠正我。

Edit: Please read Ed's answer and the comments.

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