I am building a Qt GUI application which uses QThread/QObject combinations to act as workers that do stuff outside the main thread.
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).
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.
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.
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?
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.
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?
Code:
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...
Output:
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
Expectation:
The output should end after "Exiting...".