Using worker object for QThread crashes silently or blocks main thread

0

I am attempting to make a simple factory function to wrap Python functions in QThread to prevent blocking the main application thread when performing long-running noncritical operations in the background (for example caching data).

I have attempted to follow the popular blog post that describes the actual intended usage of QThread without subclassing it and overriding run. I have been searching for the solutions to my problems for well over half of the day and can't seem to get any sort of thing working, no matter what I try and how. I'm tempted to just do this the "wrong" way and subclass QThread. The code for the wrapper follows.

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

When using the make_thread function passing the target without setting the parent to the instance of the QMainWindow, the target function executes and blocks, but then crashes silently after finished is emitted. Any print calls within the target function also fail to print anything to stdout.

Process finished with exit code -1073741819 (0xC0000005)

If the parent is set, the target runs and does print to stdout but also blocks the main thread and prevents widgets from becoming visible until finished is emitted, leaving the main window hung and blank for the duration.

The example interface I am using simply has a QLabel for an example widget that is intended to show immediately while the target executes in the background. It does not show until the target completes, and then only if parent of the QThread is specified. Otherwise, the program just crashes (after exactly five seconds, as is shown in the example below). None of this code works in either PySide2 or PyQt5.

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

If there are too many tags on this question or if they are redundant please let me know. I can never tell for sure which ones to use.

python-3.x
pyqt
pyqt5
pyside
pyside2
asked on Stack Overflow Aug 5, 2020 by spikespaz • edited Aug 5, 2020 by spikespaz

1 Answer

3

You should consider the following:

  • If a QObject has a parent then its life cycle depends on the parent, otherwise it depends on python which is handled by the concept of scope.

  • A QObject belongs to the same thread as the parent.

  • It is not thread-safe that the methods of a QObject are executed in a different thread than the one they belong to, except for the signals.

Considering the above "worker" is a QObject that does not have a parent so python will handle its memory and in this case it is a local variable that will be destroyed after executing the create_thread function. Possible solutions are passing it a QObject as a parent, making it a global variable, making it an attribute of another object that has a larger life cycle, etc.

On the other hand, worker and thread cannot have the same QObject as their parent, since by definition they live in different threads, remember that QThread is not a thread but a thread handler and belongs to the thread where it was created and not to the thread it manages.

If you are going to invoke a method of a QObject from another thread through a signal then use the @Slot decoration.

It is not necessary to use "emit" in the connection between signals.

Considering the above, you can make worker an attribute of the 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_())
answered on Stack Overflow Aug 5, 2020 by eyllanesc

User contributions licensed under CC BY-SA 3.0