Немедленно прекратите обработку очереди событий в QThread.exit () - PullRequest
0 голосов
/ 04 мая 2018

Я создаю приложение Qt GUI, которое использует комбинации QThread / QObject, чтобы действовать как рабочие, которые делают вещи вне основного потока.

Через moveToThread объект QObject перемещается в QThread. Таким образом, мой работник может иметь сигналы (это QObject) и слоты, которые обрабатываются в цикле событий (предоставляется QThread).

Теперь я хотел бы заставить рабочих вести себя особым образом, чтобы они грациозно останавливали свой поток всякий раз, когда слот в цикле событий попадает в исключение Python.

Проведя небольшое тестирование, я обнаружил, что в PyQt5 исключение в слоте вызывает остановку всего приложения, что, насколько я понимаю, является преднамеренным изменением по сравнению с PyQt4, где исключение было только напечатано, но цикл событий сохранился. Бег. Я читал, что этого можно избежать, установив собственное «exchook» на sys.excepthook, которое Qt реализует таким образом, что оно останавливает интерпретатор.

Итак, я сделал это, и пока это работает. Кроме того, исключающий крюк позволяет мне exit() моего работника, когда происходит Исключение, для которого я не нашел лучшего способа в другом месте. Я попытался создать подкласс QThread и поместить try..except вокруг вызова exec_() в методе QThread run(), но он не распространяет исключения, возникающие в цикле событий ... Таким образом, единственной оставшейся опцией было бы поставить try..except блоков внутри каждого слота, которого я хотел избежать. Или я что-то здесь упустил?

Ниже приведен MWE, демонстрирующий то, что у меня есть до сих пор. Моя проблема с этим заключается в том, что выход из потока не происходит сразу же, когда возникает исключение, продемонстрированное с помощью слота error, что приводит к вызову thread.exit() в exchook. Вместо этого будут выполняться все остальные оставшиеся события в цикле событий потока, что демонстрируется слотом 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

Expectation

Выход должен заканчиваться после «Exiting ...».

1 Ответ

0 голосов
/ 05 мая 2018

Документы 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
...