我正在构建一个Qt GUI应用程序,该应用程序使用QThread / QObject组合充当在主线程之外执行操作的工作程序。
通过moveToThread
,QObject进入QThread。这样,我的工作人员可以拥有信号(它是一个QObject)和在事件循环中处理的插槽(由QThread提供)。
现在我想让工作人员以一种特殊的方式行事,只要事件循环中的一个插槽遇到Python异常,他们就会优雅地停止他们的线程。
通过测试一下,我发现在PyQt5中,插槽中的异常导致整个应用程序停止,据我所知,这是与PyQt4相比的故意更改,其中只打印了excpetion但保留了事件循环运行。我读到可以通过monkeypatching你自己的“excepthook”到sys.excepthook
来避免这种情况,Qt以一种停止解释器的方式实现。
所以我做到了,到目前为止这是有效的。此外,当异常发生时,异常情况使我能够exit()
我的工作人员,我在其他地方找不到更好的方法。我试着在QThread的try..except
方法中对QThread进行子类化并在exec_()
的调用周围放置一个run()
,但它不会传播事件循环中出现的异常...所以唯一剩下的选项是将try..except
块放在每个插槽中,我想避免。或者我在这里错过了什么?
下面是一个MWE,它展示了我到目前为止所拥有的东西。我的问题是,当发生异常时,退出线程不会立即发生,使用error
插槽演示,导致在异常时调用thread.exit()
。而是线程事件循环中的所有其他剩余事件将被执行,这里我在其后面安排的do_work
插槽进行了演示。 exit()
似乎只是在队列中安排了另一个事件,一旦处理完,就会停止事件循环。
我该如何解决这个问题?有没有办法刷新QThread
事件的队列?我可以以某种方式优先考虑退出吗?
或者可能是另一种完全不同的方法来捕获插槽中的异常并使线程停止,而不停止主程序?
代码:
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
期望:
输出应在“退出...”之后结束。
答案 0 :(得分:2)
QThread.exit
的Qt文档有点误导:
告诉线程的事件循环以退回代码退出。
调用此函数后,线程离开事件循环和 从调用返回到QEventLoop :: exec()。 QEventLoop :: exec() function返回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