在 QThread.exit() 上立即停止处理事件队列 [英] Stop processing event-queue immediately on QThread.exit()

查看:225
本文介绍了在 QThread.exit() 上立即停止处理事件队列的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在构建一个 Qt GUI 应用程序,它使用 QThread/QObject 组合充当在主线程之外执行任务的工作线程.

I am building a Qt GUI application which uses QThread/QObject combinations to act as workers that do stuff outside the main thread.

通过moveToThread,QObject 被移动到QThread 中.这样,我的工作人员可以拥有在事件循环中处理的信号(它是一个 QObject)和插槽(由 QThread 提供).

Via moveToThread, the QObject gets moved into the QThread. This way, my worker can have signals (it's a QObject) and slots which are processed in an event loop (provided by QThread).

现在我想让工作程序以一种特殊的方式运行,以便在事件循环中的插槽遇到 Python 异常时优雅地停止线程.

Now I'd like to make the workers behave in a special way that they stop their thread gracefully whenever a slot in the event loop hits a Python exception.

通过稍微测试,我发现在 PyQt5 中,插槽中的异常会导致整个应用程序停止,据我所知,与 PyQt4 相比,这是一个有意的更改,其中仅打印了 excpetion 但保留了事件循环跑步.我读到可以通过将您自己的excepthook"猴子修补到 sys.excepthook 来避免这种情况,Qt 以一种停止解释器的方式实现.

By testing around a bit, I found that in PyQt5 an exception in a slot causes the whole application to stop, which as far as I read is an intentional change compared to PyQt4 where the excpetion was only printed but the event loop kept running. I read that it's possible to avoid that by monkeypatching your own "excepthook" to sys.excepthook, which Qt implements in a way that it stops the interpreter.

所以我这样做了,到目前为止这是有效的.此外,excepthook 使我能够在发生异常时 exit() 我的工作人员,为此我在其他地方找不到更好的方法.我尝试将 QThread 子类化并在 QThread 的 run() 方法中将 try..except 放在对 exec_() 的调用周围,但它没有't 传播发生在事件循环中的异常......所以剩下的唯一选择是将 try..except 块放在每个插槽中,我想避免这种情况.还是我错过了什么?

So I did that, and so far this works. Moreover, the excepthook enables me to exit() my worker when an Exception happens, for which I didn't find a better way elsewhere. I tried subclassing QThread and putting a try..except around the call to exec_() in the QThread's run() method, but it doesn't propagate the exceptions occuring in the event loop... So the only option left would be to put try..except blocks inside every single slot, which I wanted to avoid. Or did I miss something here?

下面是一个 MWE,它展示了我目前所拥有的.我的问题是,当异常发生时,不会立即退出线程,用 error 槽演示,这会导致调用 thread.exit()例外.相反,线程事件循环中的所有其他剩余事件都将被执行,这里由我在其后面安排的 do_work 槽演示.exit() 似乎只是将另一个事件安排到队列中,一旦它被处理,就会停止事件循环.

Below is an MWE that demonstrates what I have so far. My problem with it is that exiting the thread does not happen immediately when an Exception occurs, demonstrated with the error slot which results in a call to thread.exit() in the excepthook. Instead all other remaining events in the threads event loop will get executed, here demonstrated by the do_work slot that I scheduled behind it. exit() just seems to schedule another event to the queue which, as soon as it is processed, then stops the event loop.

我该如何解决这个问题?有没有办法刷新 QThread 事件的队列?我可以以某种方式优先退出吗?

How can I get around this? Is there a way to flush the queue of the QThread's events? Can I somehow prioritize the exit?

或者可能是另一种完全不同的方式来捕获槽中的异常并使线程停止,而不停止主程序?

Or maybe another completely different way to catch exceptions in slots and make the thread stop, without stopping the main program?

代码:

import sys
import time
from qtpy import QtWidgets, QtCore


class ThreadedWorkerBase(QtCore.QObject):
    def __init__(self):
        super().__init__()
        self.thread = QtCore.QThread(self)
        self.thread.setTerminationEnabled(False)
        self.moveToThread(self.thread)
        self.thread.start()

    def schedule(self, slot, delay=0):
        """ Shortcut to QTimer's singleShot. delay is in seconds. """
        QtCore.QTimer.singleShot(int(delay * 1000), slot)


class Worker(ThreadedWorkerBase):
    test_signal = QtCore.Signal(str)   # just for demo

    def do_work(self):
        print("starting to work")
        for i in range(10):
            print("working:", i)
            time.sleep(0.2)

    def error(self):
        print("Throwing error")
        raise Exception("This is an Exception which should stop the worker thread's event loop.")


#  set excepthook to explicitly exit Worker thread after Exception
sys._excepthook = sys.excepthook
def excepthook(type, value, traceback):
    sys._excepthook(type, value, traceback)
    thread = QtCore.QThread.currentThread()
    if isinstance(thread.parent(), ThreadedWorkerBase):
        print("This is a Worker thread. Exiting...")
        thread.exit()
sys.excepthook = excepthook

# create demo app which schedules some tasks
app = QtWidgets.QApplication([])
worker = Worker()
worker.schedule(worker.do_work)
worker.schedule(worker.error)    # this should exit the thread => no more scheduling
worker.schedule(worker.do_work)
worker.thread.wait()   # worker should exit, just wait...

输出:

starting to work
working: 0
working: 1
working: 2
working: 3
working: 4
working: 5
working: 6
working: 7
working: 8
working: 9
Throwing error
Traceback (most recent call last):
  File "qt_test_so.py", line 31, in error
    raise Exception("This is an Exception which should stop the worker thread's event loop.")
Exception: This is an Exception which should stop the worker thread's event loop.
This is a Worker thread. Exiting...
starting to work
working: 0
working: 1
working: 2
working: 3
working: 4
working: 5
working: 6
working: 7
working: 8
working: 9

期望:

输出应该在退出..."之后结束.

The output should end after "Exiting...".

推荐答案

QThread.exit 有点误导:

告诉线程的事件循环以返回码退出.

Tells the thread's event loop to exit with a return code.

调用此函数后,线程离开事件循环并从调用 QEventLoop::exec() 返回.QEventLoop::exec()函数返回返回码.

After calling this function, the thread leaves the event loop and returns from the call to QEventLoop::exec(). The QEventLoop::exec() function returns returnCode.

按照惯例,returnCode 为 0 表示成功,任何非零值表示错误.

By convention, a returnCode of 0 means success, any non-zero value indicates an error.

注意,与同名的C库函数不同,这个函数确实返回给调用者——它是事件处理停止.[强调]

Note that unlike the C library function of the same name, this function does return to the caller -- it is event processing that stops. [emphasis added]

这表明在调用 exit() 之后,将不再对线程的事件队列进行进一步处理.但事实并非如此,因为QEventLoop 总是在检查是否应该退出之前调用processEvents .这意味着当 exec() 返回时,事件队列将始终为空.

This suggests that after calling exit(), there will be no further processing of the thread's event-queue. But that is not what happens, because QEventLoop always calls processEvents before checking whether it should exit. This means that the event-queue will always be empty when exec() returns.

在您的示例中,单次定时器会将事件发布到接收线程的事件队列,最终将在其中调用连接的插槽.因此,无论您做什么,所有这些插槽都将在 线程最终退出之前被调用.

In your example, the single-shot timer will post events to the event-queue of the receiving thread, where the connected slots will eventually be called. So no matter what you do, all those slots will be called before the thread finally quits.

解决此问题的一个相当简单的方法是使用 requestInterruption 带有装饰器的功能,用于检查是否应调用插槽:

A fairly simple way to work around this is to use the requestInterruption feature with a decorator that checks to see if the slot should be called:

def interruptable(slot):
    def wrapper(self, *args, **kwargs):
        if not self.thread.isInterruptionRequested():
            slot(self, *args, **kwargs)
    return wrapper

class Worker(ThreadedWorkerBase):
    test_signal = QtCore.pyqtSignal(str)   # just for demo

    @interruptable
    def do_work(self):
        print("starting to work")
        for i in range(10):
            print("working:", i)
            time.sleep(0.2)

    @interruptable
    def error(self):
        print("Throwing error")
        raise Exception("This is an Exception which should stop the worker thread's event loop.")

def excepthook(type, value, traceback):
    sys.__excepthook__(type, value, traceback)
    thread = QtCore.QThread.currentThread()
    if isinstance(thread.parent(), ThreadedWorkerBase):
        print("This is a Worker thread. Exiting...")
        thread.requestInterruption()
        thread.exit()
sys.excepthook = excepthook

这篇关于在 QThread.exit() 上立即停止处理事件队列的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆