Python 在Maya中正确使用PySide QThread以避免硬崩溃

Python 在Maya中正确使用PySide QThread以避免硬崩溃,python,pyside,maya,qthread,Python,Pyside,Maya,Qthread,我正在尝试使用QThreads在Maya中更新自定义工具的基于Qt的UI。我有一个线程,它执行任意方法,并通过发出的信号返回结果,然后我用它来更新我的UI。下面是我的自定义QThread类: from PySide import QtCore class Thread(QtCore.QThread): result = QtCore.Signal(object) def __init__(self, parent, method, **kwargs): s

我正在尝试使用QThreads在Maya中更新自定义工具的基于Qt的UI。我有一个线程,它执行任意方法,并通过发出的信号返回结果,然后我用它来更新我的UI。下面是我的自定义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)
我传递给线程的方法是从web地址获取序列化数据的基本请求,例如:

import requests

def request_method(address):
    request = requests.get(address)
    return request.json()
下面是我如何使用自定义工具中的线程来动态更新我的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)
...
此工作流适用于其他DCC(如Nuke),但由于某些原因,它有时会导致Maya不一致地崩溃。没有错误消息,没有日志,只是硬崩溃


这让我觉得我的QThread工作流设计显然不利于Maya。您知道在使用QThread时如何最好地避免Maya崩溃,以及是什么导致了此特定问题吗?

这并不是直接回答您的
QThread
的问题,而是向您展示在Maya中使用GUI进行线程处理的另一种方法

下面是一个简单的gui示例,它有一个进度条和一个按钮。当用户单击按钮时,它将在不同的线程上创建一组worker对象来执行
time.sleep()
,并在完成时更新进度条。由于它们位于不同的线程上,因此不会从gui锁定用户,因此用户仍可在gui更新时与其交互:

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()
除了主gui之外,还有3个不同的类

  • ThreadPool
    :负责创建和管理所有辅助对象。这个类还负责用信号与gui通信,以便在工作人员完成任务时做出相应的反应
  • Worker
    :这就是实际的繁重工作以及您希望在线程中处理的任何内容
  • ThreadSignals
    :这在工作线程内部使用,以便在完成后能够与池通信。worker类不是由
    QObject
    继承的,这意味着它本身不能发出信号,因此这是一种解决方法

  • 我知道这一切看起来都很冗长,但它似乎在一系列不同的工具中运行良好,没有任何硬崩溃。

    我们工作室的一位工程师发现了一些与Python线程和PyQt/PySide的使用有关的bug。请参阅:

    • [PySide 1.x]
    • [PySide 2.x]
    记者手记:

    尽管QObject是可重入的,但GUI类,尤其是QWidget及其所有子类,是不可重入的。它们只能从主线程使用


    为了进一步澄清崩溃情况,如果我运行工具一次,它不会崩溃Maya,但如果我关闭它并再次运行相同的代码,Maya会在执行问题中概述的线程方法时崩溃…Maya说使用线程是不安全的,我从不在线程中冒险,所以我不知道是否有解决方法:@DrWeeny谢谢您的评论。该链接引用运行maya.cmds命令的常规Python线程。我知道这是非常不安全的-在我目前的情况下,我没有调用任何maya.cmds方法,而是通过QThreads发出GET请求从服务器获取数据,这应该是安全的,但显然不是…感谢您提供了这个替代方案,我以前没有使用过此工作流。我将在我的工具中测试它,看看它是否表现得更好。我测试了您提出的替代方案,虽然它确实似乎使线程更稳定,但Maya仍然无法预测地崩溃,因此我无法将此标记为答案。谢谢你的分享,我会在其他脚本中使用这个工作流。你认为你可以用一个最小的仍然崩溃的完整示例来编辑你的文章吗?如果我也能确认崩溃,那就太好了。除了我最初的问题,我真的无法得到任何更具体的答案,基本上我有一个方法来发出get请求,并从服务器获取JSON响应数据,我想线程化。响应数据是一个简单的键值字典,我用它来填充QTableWidget行和一些QComboBox元素,仅此而已。令人沮丧的是,我无法确定导致崩溃的任何特定请求,每个会话的请求都不同。其他人认为可能是错误/异常导致了崩溃,因为假定QThreads/QRunnables不能很好地处理这些请求,但我所有的请求和UI操作在主线程上都可以正常工作,所以我真的不确定这个问题是从哪里来的。冷静点!