Правильное использование PySide QThread в Maya, чтобы избежать крушения - PullRequest
1 голос
/ 17 июня 2019

Я пытаюсь использовать QThreads для обновления пользовательского интерфейса на основе Qt моего собственного инструмента в Maya. У меня есть поток, который выполняет произвольные методы и возвращает результат через излучаемый сигнал, который я затем использую для обновления своего пользовательского интерфейса. Вот мой собственный класс QThread:

from PySide import QtCore


class Thread(QtCore.QThread):

    result = QtCore.Signal(object)

    def __init__(self, parent, method, **kwargs):
        super(Thread, self).__init__(parent)
        self.parent = parent
        self.method = method
        self.kwargs = kwargs

    def run(self):
        result = self.method(**self.kwargs)
        self.result.emit(result)

Методы, которые я передаю в поток, - это основные запросы для получения сериализованных данных с веб-адреса, например:

import requests

def request_method(address):
    request = requests.get(address)
    return request.json()

И вот как я использую нить в своем пользовательском инструменте для динамического обновления моего интерфейса:

...
    thread = Thread(parent=self, method=request_method, address='http://www.example.com/')
    thread.result.connect(self._slot_result)
    thread.start()

def _slot_result(self, result):
    # Use the resulting data to update some UI element:
    self.label.setText(result)
...

Этот рабочий процесс работает в других DCC, таких как Nuke, но по какой-то причине он иногда приводит к сбою в Maya. Нет сообщения об ошибке, нет журнала, просто тяжелый сбой.

Это заставляет меня думать, что мой дизайн рабочего процесса QThread явно не дружествен к майя. Любые идеи, как лучше избежать краха Maya при использовании QThreads и что может быть причиной этой конкретной проблемы?

1 Ответ

1 голос
/ 18 июня 2019

Это не дает прямого ответа на то, что происходит с вашим QThread, но показывает вам другой способ работы с потоками с помощью GUI в Maya.

Вот простой пример графического интерфейса, в котором есть прогресспанель и кнопка.Когда пользователь нажимает кнопку, он создает группу рабочих объектов в другом потоке для выполнения time.sleep() и обновляет индикатор выполнения по завершении.Так как они находятся в другом потоке, он не будет блокировать пользователя из графического интерфейса, поэтому они могут взаимодействовать с ним по мере его обновления:

from functools import partial
import traceback
import time

from PySide2 import QtCore
from PySide2 import QtWidgets


class Window(QtWidgets.QWidget):

    """
    Your main gui class that contains a progress bar and a button.
    """

    def __init__(self, parent=None):
        super(Window, self).__init__(parent)

        # Create our main thread pool object that will handle all the workers and communication back to this gui.
        self.thread_pool = ThreadPool(max_thread_count=5)  # Change this number to have more workers running at the same time. May need error checking to make sure enough threads are available though!
        self.thread_pool.pool_started.connect(self.thread_pool_on_start)
        self.thread_pool.pool_finished.connect(self.thread_pool_on_finish)
        self.thread_pool.worker_finished.connect(self.worker_on_finish)

        self.progress_bar = QtWidgets.QProgressBar()

        self.button = QtWidgets.QPushButton("Run it")
        self.button.clicked.connect(partial(self.thread_pool.start, 30))  # This is the number of iterations we want to process.

        self.main_layout = QtWidgets.QVBoxLayout()
        self.main_layout.addWidget(self.progress_bar)
        self.main_layout.addWidget(self.button)
        self.setLayout(self.main_layout)

        self.setWindowTitle("Thread example")
        self.resize(500, 0)

    def thread_pool_on_start(self, count):
        # Triggers right before workers are about to be created. Start preparing the gui to be in a "processing" state.
        self.progress_bar.setValue(0)
        self.progress_bar.setMaximum(count)

    def thread_pool_on_finish(self):
        # Triggers when all workers are done. At this point you can do a clean-up on your gui to restore it to it's normal idle state.
        if self.thread_pool._has_errors:
            print "Pool finished with no errors!"
        else:
            print "Pool finished successfully!"

    def worker_on_finish(self, status):
        # Triggers when a worker is finished, where we can update the progress bar.
        self.progress_bar.setValue(self.progress_bar.value() + 1)


class ThreadSignals(QtCore.QObject):

    """
    Signals must inherit from QObject, so this is a workaround to signal from a QRunnable object.
    We will use signals to communicate from the Worker class back to the ThreadPool.
    """

    finished = QtCore.Signal(int)


class Worker(QtCore.QRunnable):

    """
    Executes code in a seperate thread.
    Communicates with the ThreadPool it spawned from via signals.
    """

    StatusOk = 0
    StatusError = 1

    def __init__(self):
        super(Worker, self).__init__()
        self.signals = ThreadSignals()

    def run(self):
        status = Worker.StatusOk

        try:
            time.sleep(1)  # Process something big here.
        except Exception as e:
            print traceback.format_exc()
            status = Worker.StatusError

        self.signals.finished.emit(status)


class ThreadPool(QtCore.QObject):

    """
    Manages all Worker objects.
    This will receive signals from workers then communicate back to the main gui.
    """

    pool_started = QtCore.Signal(int)
    pool_finished = QtCore.Signal()
    worker_finished = QtCore.Signal(int)

    def __init__(self, max_thread_count=1):
        QtCore.QObject.__init__(self)

        self._count = 0
        self._processed = 0
        self._has_errors = False

        self.pool = QtCore.QThreadPool()
        self.pool.setMaxThreadCount(max_thread_count)

    def worker_on_finished(self, status):
        self._processed += 1

        # If a worker fails, indicate that an error happened.
        if status == Worker.StatusError:
            self._has_errors = True

        if self._processed == self._count:
            # Signal to gui that all workers are done.
            self.pool_finished.emit()

    def start(self, count):
        # Reset values.
        self._count = count
        self._processed = 0
        self._has_errors = False

        # Signal to gui that workers are about to begin. You can prepare your gui at this point.
        self.pool_started.emit(count)

        # Create workers and connect signals to gui so we can update it as they finish.
        for i in range(count):
            worker = Worker()
            worker.signals.finished.connect(self.worker_finished)
            worker.signals.finished.connect(self.worker_on_finished)
            self.pool.start(worker)


def launch():
    global inst
    inst = Window()
    inst.show()

Помимо основного графического интерфейса, есть 3 различных класса.

  1. ThreadPool: отвечает за создание и управление всеми рабочими объектами.Этот класс также отвечает за обратную связь с графическим интерфейсом с помощью сигналов, чтобы он мог реагировать соответствующим образом, пока работники заканчивают работу.
  2. Worker: это то, что фактически выполняет тяжелую работу и все, что вы хотите обработать в потоке.
  3. ThreadSignals: используется внутри работника, чтобы иметь возможность связаться с пулом, когда это будет сделано.Рабочий класс не наследуется QObject, что означает, что он не может излучать сигналы сам по себе, поэтому это используется как обходной путь.

Я знаю, что все это выглядит многословно, ноПохоже, что он отлично работает в куче различных инструментов без каких-либо серьезных сбоев.

...