PySide槽函数未在对象线程中运行

3
我正在尝试在PySide中使用单独的线程运行长时间任务,以便主线程可以继续处理GUI事件。我已经阅读了正确的方法:

  • 将任务封装在QObject子类中,在其中使用run()方法完成工作,并在完成后发出finished信号。
  • 创建一个新的QThread,并使用QObject.moveToThread()将任务对象的线程亲和性设置为它。
  • 将线程的start信号连接到任务的run()方法。
  • 使用QThread.start()启动线程。

然而,我遇到了一个奇怪的问题,如果将run()方法包装在一个槽中,则该函数将在主线程中运行,而不是属于该对象的线程。如果将其保留为标准Python方法,则一切正常。

这是我创建的最简示例:

#!/usr/bin/env python

import sys

from PySide import QtCore, QtGui


class Task(QtCore.QObject):
    """Does some work and emits a signal when done."""

    finished = QtCore.Signal(object)

    def run1(self):
        """Runs task and emits finished() signal when done."""
        try:
            # Try running the task
            result = self._run()

        except:
            self.finished.emit(None)

        else:
            self.finished.emit(result)

    @QtCore.Slot()
    def run2(self):
        """Same as run1, but wrapped in a slot."""
        self.run1()

    def _run(self):
        """Override in subclass"""
        pass


class TestTask(Task):
    """Prints thread ID."""

    def __init__(self, name):
        super().__init__()
        self.name = name

    def _run(self):
        print('{} thread ID:'.format(self.name), QtCore.QThread.currentThreadId())
        return 'success'


def main():

    gui = QtGui.QApplication([])

    print('Main thread ID: ', QtCore.QThread.currentThreadId())

    # thread1 calls task1.run1()
    task1 = TestTask('task1')
    task1.finished.connect(lambda r: print('Task 1 finished:', r))

    thread1 = QtCore.QThread()
    task1.moveToThread(thread1)

    thread1.started.connect(task1.run1)

    # thread2 calls task2.run2()
    task2 = TestTask('task2')
    task1.finished.connect(lambda r: print('Task 2 finished:', r))

    thread2 = QtCore.QThread()
    task2.moveToThread(thread2)

    thread2.started.connect(task2.run2)

    # Start both threads
    thread1.start()
    thread2.start()

    # Run event loop (doesn't actually return)
    sys.exit(gui.exec_())


if __name__ == '__main__':
    main()

这会生成以下输出:
Main thread ID:  139962303178496
task1 thread ID: 139961642776320
task2 thread ID: 139962303178496
Task 2 finished success
Task 1 finished success

run()作为Python标准方法离开并不是什么大问题,但我想知道为什么会这样。这是使用QT4.8和PySide 1.2.4。

1个回答

3
这可能是PySide中的一个潜在错误导致的。该问题似乎是由继承了带有装饰槽的基类引起的。如果将此槽移动到子类中,则问题就会消失:
class TestTask(Task):
    ...

    @QtCore.Slot()
    def run2(self):
        """Same as run1, but wrapped in a slot."""
        self.run1()

(顺便提一句,值得注意的是,您最初的例子在PyQt4中可以正常工作。)

更新:

正如预期的那样,这是由于PySide中已知的一个错误所导致的:请见PYSIDE-249


实际上,拥有带有签名(object)的插槽是我最初的设想(也起作用),但这是一个错误,留下了当我将方法带有参数时的痕迹。QThread.started信号没有任何参数,所以插槽也不应该有参数。使用@QtCore.Slot(int, str, object, int)装饰run2也可以工作,尽管签名明显不正确。我认为因为签名不匹配信号,所以它被视为普通的Python函数而不是插槽。 - JaredL
@JaredL。抱歉 - 我早上第一时间写了这个答案,显然没有检查我的推理。不过很奇怪的是,我的错误解释仍然“起作用”。无论如何,我再次看了一下,我认为我已经找到了问题的真正根源 - 请参见我的更新答案。 - ekhumoro
对我来说,这似乎大部分是正确的。在将其连接到基类上的'run2()'插槽时,我已验证一切按预期工作(在打印了其线程ID后)。但是,在继承插槽的子类中失败了。然而,当在子类中重新定义插槽时,插槽仍在主线程中运行。我已经尝试使用新的conda环境进行过此操作:一个带有PySide 1.2.2和Python 2.7(conda create -n pyside-27 pyside),另一个是从conda-forge渠道带有PySide 1.2.4和Python 3.6 (conda create -n pyside-36 -c conda-forge pyside python=3). - JaredL
@JaredL。你应该始终将测试用例作为独立脚本运行,而不是在IDE或调试器中运行。在我的测试中,在子类中定义的修饰符插槽总是在工作线程中运行,而如果从基类继承,则在主线程中运行。这是使用python 3.6.2、qt 4.8.7、shiboken 1.2.4、pyside 1.2.4,在正常控制台(Linux上)执行脚本。请以类似的方式测试脚本。 - ekhumoro
@JaredL。我现在确认这是pyside中的一个bug:请查看我的回答中的更新。 - ekhumoro
看起来就是这样了!我想我现在只能坚持使用简单的方法。 - JaredL

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接