首页 > 解决方案 > 在 QThread.exit() 上立即停止处理事件队列

问题描述

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

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

现在我想让工作人员以一种特殊的方式行事,只要事件循环中的插槽遇到 Python 异常,他们就会优雅地停止线程。

通过测试,我发现在 PyQt5 中,插槽中的异常会导致整个应用程序停止,据我所知,与仅打印异常但事件循环继续运行的 PyQt4 相比,这是一个有意的更改。我读到可以通过将您自己的“excepthook”monkeypatching 来避免这种情况sys.excepthook,Qt 以它停止解释器的方式实现。

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

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

我怎样才能解决这个问题?有没有办法刷新QThread's 事件的队列?我能以某种方式优先考虑退出吗?

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

代码

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

期望

输出应在“Exiting...”之后结束。

标签: multithreadingpython-3.xpyqtqthreadevent-loop

解决方案


Qt 文档QThread.exit有点误导:

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

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

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

请注意,与同名的 C 库函数不同,此函数确实返回给调用者——它是停止的事件处理[重点补充]

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

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

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

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

推荐阅读