将 worker 对象用于 QThread 会静默崩溃或阻塞主线程

Using worker object for QThread crashes silently or blocks main thread

我正在尝试创建一个简单的工厂函数来将 Python 函数包装在 QThread 中,以防止在后台执行长时间 运行 非关键操作时阻塞主应用程序线程(例如缓存数据)。

我试图遵循描述 QThread 实际预期用途的 popular blog post,而不对其进行子类化和覆盖 run。半天多的时间里,我一直在寻找问题的解决方案,但无论我尝试什么以及如何尝试,似乎都无法解决问题。我很想以“错误”的方式和子类 QThread 来做这件事。包装器的代码如下。

from qtpy.QtCore import Slot, Signal, QThread, QObject


class Worker(QObject):
    finished = Signal()

    def __init__(self, target, *args, parent=None, **kwargs):
        super().__init__(parent)

        self.__target = target
        self.__args = args
        self.__kwargs = kwargs

    def run(self):
        self.__target(*self.__args, **self.__kwargs)
        self.finished.emit()


def create_thread(target, *args, parent=None, **kwargs):
    thread = QThread(parent)
    worker = Worker(target, *args, **kwargs, parent=parent)

    worker.moveToThread(thread)
    thread.started.connect(worker.run)
    worker.finished.connect(thread.quit)
    worker.finished.connect(worker.deleteLater)
    thread.finished.connect(thread.deleteLater)

    return thread

当使用 make_thread 函数传递 target 而不将 parent 设置为 QMainWindow 的实例时,target 函数执行并阻塞, 但在发出 finished 后静默崩溃。 target 函数中的任何 print 调用也无法将任何内容打印到 stdout

Process finished with exit code -1073741819 (0xC0000005)

如果设置了 parenttarget 会运行并打印到 stdout,但也会阻塞主线程并阻止小部件在 finished 之前变得可见发出,在持续时间内让主要 window 挂起并空白。

我正在使用的示例界面只有一个 QLabel 作为示例小部件,旨在在 target 在后台执行时立即显示。它在 target 完成之前不会显示,然后只有在指定 QThreadparent 时才会显示。否则,程序就会崩溃(恰好五秒钟后,如下例所示)。此代码的 None 适用于 PySide2PyQt5

import sys
import time

from qtpy.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxLayout

import modpack_builder.gui.helpers as helpers


class ExampleMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setCentralWidget(QWidget(self))
        self.centralWidget().setLayout(QVBoxLayout(self.centralWidget()))

        self.label = QLabel("If you see this immediately, the thread did not block.", self.centralWidget())

        self.centralWidget().layout().addWidget(self.label)

        self.test_thread = helpers.create_thread(self.long_running_task)
        self.test_thread.start()

    @staticmethod
    def long_running_task():
        print("Task started.")
        time.sleep(5)
        print("Task finished.")


if __name__ == "__main__":
    app = QApplication(list())
    window = ExampleMainWindow()

    window.show()
    sys.exit(app.exec_())

如果这个问题的标签太多或者多余,请告诉我。我永远无法确定要使用哪些。

您应该考虑以下几点:

  • 如果一个QObject有一个父对象,那么它的生命周期就依赖于父对象,否则它就依赖于python,这是由作用域的概念处理的。

  • QObject 与父对象属于同一个线程。

  • 不是thread-safe QObject 的方法在与其所属线程不同的线程中执行,信号除外。

考虑到上面的“worker”是一个没有父对象的 QObject,所以 python 将处理它的内存,在这种情况下它是一个局部变量,在执行 create_thread 功能。可能的解决方案是将 QObject 作为父对象传递给它,使其成为全局变量,使其成为另一个具有更大生命周期的对象的属性等。

另一方面,worker 和 thread 不能拥有与其父级相同的 QObject,因为根据定义它们生活在不同的线程中,请记住 QThread 不是线程而是线程处理程序并且属于它所在的线程创建而不是它管理的线程。

如果要通过信号从另一个线程调用 QObject 的方法,请使用 @Slot 装饰。

信号之间的连接不需要用“emit”

综上所述,可以将worker作为QThread的一个属性。

import sys
import time

from qtpy.QtCore import Slot, Signal, QThread, QObject
from qtpy.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxLayout


class Worker(QObject):
    start = Signal()
    finished = Signal()

    def __init__(self, target, *args, parent=None, **kwargs):
        super().__init__(parent)

        self.__target = target
        self.__args = args
        self.__kwargs = kwargs

        self.start.connect(self.run)

    @Slot()
    def run(self):
        self.__target(*self.__args, **self.__kwargs)
        self.finished.emit()


def create_thread(target, *args, parent=None, **kwargs):
    thread = QThread(parent)
    worker = Worker(target, *args, **kwargs)
    worker.moveToThread(thread)
    thread.started.connect(worker.start)
    # or
    # thread.started.connect(worker.run)
    worker.finished.connect(thread.quit)
    worker.finished.connect(worker.deleteLater)
    thread.finished.connect(thread.deleteLater)

    thread.worker = worker

    return thread


class ExampleMainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setCentralWidget(QWidget(self))
        self.centralWidget().setLayout(QVBoxLayout(self.centralWidget()))

        self.label = QLabel("If you see this immediately, the thread did not block.",)

        self.centralWidget().layout().addWidget(self.label)

        self.test_thread = create_thread(self.long_running_task)
        self.test_thread.start()

    @staticmethod
    def long_running_task():
        print("Task started.")
        time.sleep(5)
        print("Task finished.")


if __name__ == "__main__":
    app = QApplication(list())
    window = ExampleMainWindow()

    window.show()
    sys.exit(app.exec_())