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?
One of the engineers at our studio discovered a few bugs related to the use of Python threads and PyQt/PySide. Please refer to:
- [PySide 1.x] https://bugreports.qt.io/browse/PYSIDE-810
- [PySide 2.x] https://bugreports.qt.io/browse/PYSIDE-813
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.
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.
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.
Worker
: This is what does the actual heavy lifting and whatever you want to process in the thread.
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.