4

I'm attempting to use QThreads to update my custom tool's Qt-based UI inside of Maya. I have a thread that executes arbitrary methods and returns the result via an emitted signal, which I then use to update my UI. Here's my custom QThread class:

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)

The methods I'm passing to the thread are basic requests for getting serialized data from a web address, for example:

import requests

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

And here is how I use the thread in my custom tool to dynamically update my UI:

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

This workflow works in other DCCs like Nuke, but for some reason it causes Maya to sometimes crash inconsistently. No error message, no log, just a hard crash.

This makes me think that my QThread workflow design is obviously not Maya-friendly. Any ideas how best to avoid crashing Maya when using QThreads and what may be causing this particular issue?

Viktor Petrov
  • 444
  • 4
  • 13
  • To further clarify the crash circumstances, if I run my tool once, it doesn't crash Maya, but if I close it and run the same code again, Maya crashes on executing the threading method outlined in my question... – Viktor Petrov Jun 17 '19 at 10:42
  • maya is saying that using thread is unsafe, I never adventure myself in thread so I cant tell if there is workarounds : https://knowledge.autodesk.com/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2015/ENU/Maya/files/Python-Python-and-threading-htm.html – DrWeeny Jun 17 '19 at 13:15
  • @DrWeeny thanks for commenting. That link refers to regular Python threads running maya.cmds commands. I'm aware that's very unsafe - in my current case I'm not calling any maya.cmds methods, but instead making GET requests to fetch data from a server via QThreads, which *should* be safe, but it's obviously not... – Viktor Petrov Jun 18 '19 at 02:02

2 Answers2

2

This doesn't answer directly what's going on with your QThread, but to show you another way to go about threading with guis in Maya.

Here's a simple example of a gui that has a progress bar and a button. When the user clicks the button it will create a bunch of worker objects on a different thread to do a time.sleep(), and will update the progress bar as they finish. Since they're on a different thread it won't lock the user from the gui so they can still interact with it as it updates:

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

Aside from the main gui, there's 3 different classes.

  1. ThreadPool: This is responsible to create and manage all worker objects. This class is also responsible to communicate back to the gui with signals so it can react accordingly while workers are completing.
  2. Worker: This is what does the actual heavy lifting and whatever you want to process in the thread.
  3. ThreadSignals: This is used inside the worker to be able to communicate back to the pool when it's done. The worker class isn't inherited by QObject, which means it can't emit signals in itself, so this is used as a work around.

I know this all looks long winded, but it seems to be working fine in a bunch of different tools without any hard crashes.

Green Cell
  • 4,677
  • 2
  • 18
  • 49
  • Thank you for this alternative, I haven't used this workflow before. I'll test it in my tool to see if it behaves better. – Viktor Petrov Jun 18 '19 at 03:59
  • I tested your proposed alternative, and while it did seem to make threading a bit more stable, Maya still hard crashed unpredictably, so I cannot mark this as the answer. Thanks for sharing though, I'll make use of this workflow in other scripts. – Viktor Petrov Jun 24 '19 at 04:22
  • Do you think you can edit your post with a minimal complete example that still crashes? It would be nice to see if I can confirm a crash on my end too. – Green Cell Jun 24 '19 at 15:44
  • I can't really get any more specific than my original question, essentially I have a method for making a GET request and getting JSON response data from a server, which I want to thread. The response data is a simple key - value dictionary, which I use to populate a QTableWidget rows and some QComboBox elements, that's all. It's frustrating that I cannot pinpoint any specific request that causes the crash, it varies from session to session. – Viktor Petrov Jun 25 '19 at 03:29
  • Others have suggested it might be errors/exceptions that cause the crash, since supposedly QThreads/QRunnables don't handle these well, but all my requests and UI manipulation all work with no problem on the main thread, so I'm really not sure where the issue might be coming from. – Viktor Petrov Jun 25 '19 at 03:31
2

One of the engineers at our studio discovered a few bugs related to the use of Python threads and PyQt/PySide. Please refer to:

Notes from the reporter:

Although QObject is reentrant, the GUI classes, notably QWidget and all its subclasses, are not reentrant. They can only be used from the main thread.

jdi
  • 90,542
  • 19
  • 167
  • 203