关于多线程:立即在QThread.exit()上停止处理事件队列

Stop processing event-queue immediately on QThread.exit()

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

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

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

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

所以我做到了,到目前为止,这是可行的。此外,当发生异常时,excepthook使我能够exit()我的工作人员,对此我在其他地方找不到更好的方法。我尝试对QThread进行子类化,并在QThread的run()方法中对exec_()的调用周围放置了try..except,但是它不会传播事件循环中发生的异常...因此,剩下的唯一选择就是放置我想避免在每个插槽中的try..except块。还是我想念这里的东西?

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

我该如何解决?有没有一种方法可以清除QThread事件的队列?我可以以某种方式优先处理出口吗?

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

码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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...

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 ..."之后结束。


QThread.exit的Qt文档有些令人误解:

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

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

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

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()返回时,事件队列将始终为空。

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

解决此问题的一种非常简单的方法是将requestInterruption功能与装饰器一起使用,以检查是否应调用该插槽:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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