Использование рабочего объекта для QThread приводит к сбою или блокировке основного потока - PullRequest
0 голосов
/ 06 августа 2020

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

Я попытался подписаться на популярную запись в блоге , в которой описывается фактическое предполагаемое использование QThread без подкласса и отмены 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. Любые вызовы print в функции target также не выводят ничего на stdout.

Process finished with exit code -1073741819 (0xC0000005)

Если установлено parent, запускается target и выводит на stdout но также блокирует основной поток и не позволяет виджетам становиться видимыми до тех пор, пока не будет выпущено finished, оставляя главное окно зависшим и пустым на время.

Пример интерфейса, который я использую, просто имеет QLabel для пример виджета, который предназначен для немедленного показа, пока target выполняется в фоновом режиме. Он не отображается, пока не завершится target, и только если указано parent из QThread. В противном случае программа просто вылетает (ровно через пять секунд, как показано в примере ниже). Ни один из этих кодов не работает ни в PySide2, ни в 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_())

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

1 Ответ

3 голосов
/ 06 августа 2020

Следует учитывать следующее:

  • Если у QObject есть родительский объект, то его жизненный цикл зависит от родителя, в противном случае он зависит от python, который обрабатывается концепцией область.

  • QObject принадлежит тому же потоку, что и родительский.

  • Это не является поточно-ориентированным, когда методы QObject выполняется в другом потоке, чем тот, к которому они принадлежат, за исключением сигналов.

Учитывая, что вышеупомянутый "worker" - это QObject, у которого нет родителя, поэтому python будет обрабатывать его память, и в данном случае это локальная переменная, которая будет уничтожена после выполнения функции create_thread. Возможные решения - передать ему QObject в качестве родительского объекта, сделать его глобальной переменной, сделать его атрибутом другого объекта с большим жизненным циклом, et c.

С другой стороны, 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_())
...