PySide2:清理孙子线程的正确方法

PySide2: Proper way to clean up grandchild threads

我正在使用 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 断点处停止。那么在析构函数中暂停是否允许信号在子线程中被捕获?无论如何,没有断点这就不起作用,所以那是行不通的。

我删除了所有这些并做了一些看起来非常愚蠢的事情(从长远来看):

    # 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 函数(在这个玩具和我的实际代码中)是隐式线程安全的。

所以这引出了我的问题:

经过更多实验后,如果您将连接设为 Qt.BlockingQueuedConnection,signals/slots 方法 似乎 会起作用。

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()