PySide2: правильный способ очистки потоков внуков - PullRequest
0 голосов
/ 04 февраля 2020

Я использую PySide2 для написания многопоточного приложения. У меня есть объект Controller, уже запущенный в его собственном потоке, чтобы он не блокировал поток GUI. Этот контроллер должен раскрутить несколько рабочих потоков, которые выполняют работу непрерывно. Я могу вручную запускать и останавливать эти потоки внуков с помощью сигнала, но у меня возникают проблемы с их очисткой сигналами при выходе из приложения.

Вот пример с игрушкой, который повторяет мою проблему:

import sys

import shiboken2
from PySide2.QtCore import QObject, QThread
from PySide2.QtWidgets import QApplication, QPushButton


class Grandchild(QObject):
    def __init__(self, parent=None):
        super(Grandchild, self).__init__(parent)
        print('Grandchild()')

    def __del__(self):
        print('~Grandchild()')


class Child(QObject):
    _thread = None
    _worker = None

    def __init__(self, parent=None):
        super(Child, self).__init__(parent)
        print('Child()')

    def __del__(self):
        print('~Child()')
        if shiboken2.isValid(self._thread):
            self.stop_thread()

    def start_thread(self):
        print('Starting grandchild thread')
        self._thread = QThread(self)
        self._worker = Grandchild()
        self._worker.moveToThread(self._thread)
        self._thread.finished.connect(self._worker.deleteLater)
        self._thread.start()

    def stop_thread(self):
        print('Stopping grandchild thread')
        self._thread.quit()
        self._thread.wait()

    def toggle_thread(self):
        if self._thread and self._thread.isRunning():
            self.stop_thread()
        else:
            self.start_thread()


class Parent(QPushButton):
    _thread = None
    _worker = None

    def __init__(self, parent=None):
        super(Parent, self).__init__(parent)
        print('Parent()')
        self.setText('Start Grandchild')

        self._thread = QThread(self)
        self._worker = Child()
        self._worker.moveToThread(self._thread)
        self._thread.finished.connect(self._worker.deleteLater)
        self._thread.start()

        self.clicked.connect(self.on_push)
        self.clicked.connect(self._worker.toggle_thread)

    def __del__(self):
        print('~Parent()')
        if shiboken2.isValid(self._thread):
            self._thread.quit()
            self._thread.wait()

    def on_push(self):
        if self.text() == 'Start Grandchild':
            self.setText('Stop Grandchild')
        else:
            self.setText('Start Grandchild')


def main():
    app = QApplication(sys.argv)

    widget = Parent()
    widget.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

В этом примере запуск и остановка потоков с помощью кнопки работает нормально. Если я запускаю и останавливаю внука с помощью кнопки, а затем закрываю приложение, все деструкторы вызывают правильно, и приложение корректно завершает работу. Если я только запускаю внука и закрываю приложение, я получаю:

Parent()
Child()
Starting grandchild thread
Grandchild()
QThread: Destroyed while thread is still running
~Parent()

Process finished with exit code 134 (interrupted by signal 6: SIGABRT)

Моей первой мыслью было изменить Parent следующим образом:

    stop_child = Signal()

    def __init__(self):
        self.stop_child.connect(self._worker.stop_thread)


    def __del__(self):
        print('~Parent()')
        self.stop_child.emit()
        if shiboken2.isValid(self._thread):
            self._thread.quit()
            self._thread.wait()

Как есть, это не влияет проблема вообще. Если я поставлю точку останова в Child.stop_thread, я смогу увидеть, что она никогда не будет выполнена до того, как будет сгенерирован SIGABRT. Если я поставлю точку останова внутри Parent.__del__, выполнение остановится, как и ожидалось. Если я возобновлю выполнение, остановится на моей точке останова Child.stop_thread. Итак, что-то в паузе в деструкторе - позволить сигналу попасть в поток Child? В любом случае, это не работает без точек останова, так что это не работает.

Я удалил все это и сделал что-то, что кажется действительно глупым (в долгосрочной перспективе):

    # Parent
    def __del__(self):
        print('~Parent()')
        self._worker.stop_thread() # Call the instance fn directly
        if shiboken2.isValid(self._thread):
            self._thread.quit()
            self._thread.wait()

И, конечно, это работает:

Parent()
Child()
Starting grandchild thread
Grandchild()
~Parent()
Stopping grandchild thread
~Child()
~Grandchild()

Process finished with exit code 0

Кажется, что было бы действительно ужасной идеей вызывать функцию экземпляра для объекта, который живет в другом потоке. Я предполагаю, что это работает, потому что моя функция stop_child (как в этой игрушке, так и в моем реальном коде) неявно поточно-ориентирована.

Итак, это подводит меня к моим вопросам:

  • Как правильно очистить потоки внуков, когда основной поток завершает работу?
  • Что если, как в этом примере, я хочу, чтобы эти потоки внуков абстрагировались за объектом-членом?
  • Разве подход с использованием сигналов / слотов не сработал?

1 Ответ

0 голосов
/ 07 февраля 2020

После дополнительных экспериментов кажется, что подход к сигналам / слотам будет работать, если вы сделаете соединение Qt.BlockingQueuedConnection.

self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)

Это означает, что основной поток блокируется до тех пор, пока сигнал не будет доставлен, что может быть нежелательно, если сигнал stop_child используется во время нормальной работы во время выполнения. Хотя в деструкторе основного виджета все в порядке.

Полный рабочий пример приведен ниже:

import sys

import shiboken2
from PySide2.QtCore import QObject, QThread, Signal, Qt
from PySide2.QtWidgets import QApplication, QPushButton


class Grandchild(QObject):
    def __init__(self, parent=None):
        super(Grandchild, self).__init__(parent)
        print('Grandchild()')

    def __del__(self):
        print('~Grandchild()')


class Child(QObject):
    _thread = None
    _worker = None

    def __init__(self, parent=None):
        super(Child, self).__init__(parent)
        print('Child()')

    def __del__(self):
        print('~Child()')
        if shiboken2.isValid(self._thread):
            self.stop_thread()

    def start_thread(self):
        print('Starting grandchild thread')
        self._thread = QThread(self)
        self._worker = Grandchild()
        self._worker.moveToThread(self._thread)
        self._thread.finished.connect(self._worker.deleteLater)
        self._thread.start()

    def stop_thread(self):
        print('Stopping grandchild thread')
        self._thread.quit()
        self._thread.wait()

    def toggle_thread(self):
        if self._thread and self._thread.isRunning():
            self.stop_thread()
        else:
            self.start_thread()


class Parent(QPushButton):
    _thread = None
    _worker = None

    stop_child = Signal()

    def __init__(self, parent=None):
        super(Parent, self).__init__(parent)
        print('Parent()')
        self.setText('Start Grandchild')

        self._thread = QThread(self)
        self._worker = Child()
        self._worker.moveToThread(self._thread)
        self._thread.finished.connect(self._worker.deleteLater)
        self._thread.start()

        self.clicked.connect(self.on_push)
        self.clicked.connect(self._worker.toggle_thread)
        self.stop_child.connect(self._worker.stop_thread, Qt.BlockingQueuedConnection)

    def __del__(self):
        print('~Parent()')
        self.stop_child.emit()
        if shiboken2.isValid(self._thread):
            self._thread.quit()
            self._thread.wait()

    def on_push(self):
        if self.text() == 'Start Grandchild':
            self.setText('Stop Grandchild')
        else:
            self.setText('Start Grandchild')


def main():
    app = QApplication(sys.argv)

    widget = Parent()
    widget.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
...