PyQt | 信号在QThread中未被处理,而是在主线程中处理

4
在这个简单的PyQt演示程序中,我从主线程发出信号。在工作线程中,我连接到它们,但是信号处理程序在主线程中运行:
from PyQt4 import QtGui, QtCore
import threading
from time import sleep
import sys


class Data():
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __str__(self):
        return "Data having %d and %d" % (self.a, self.b)

class Worker(QtCore.QThread):
    def __init__(self, parent):
        QtCore.QThread.__init__(self)
        self.p = parent

    def run(self):
        self.connect(self.p, QtCore.SIGNAL("newTask"), self.task)
        print "[%s] running exec_()" % threading.currentThread()
        self.exec_()

    def task(self, dataobj):
        print "[%s] Processing" % threading.currentThread(), dataobj
        sleep(3)
        print "Done with", dataobj
        self.emit(QtCore.SIGNAL("taskDone"), str(dataobj))

class App(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)
        self.w = Worker(self)
        self.connect(self.w, QtCore.SIGNAL("taskDone"), self.on_task_done)
        self.w.start()

    def assign_tasks(self):
        self.emit(QtCore.SIGNAL("newTask"), Data(3, 4))
        self.emit(QtCore.SIGNAL("newTask"), Data(5, 6))
        print "[%s] Tasks sent" % threading.currentThread()

    @staticmethod
    def on_task_done(objstr):
        print "[%s] App: Worker finished with" % threading.currentThread(), objstr

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    a = App()
    sleep(1)
    a.assign_tasks()
    sleep(20)
    sys.exit(app.exec_())

但是结果显示回调函数在主线程中运行:
[<_DummyThread(Dummy-1, started daemon 105564)>] running exec_()
[<_MainThread(MainThread, started 105612)>] Processing Data having 3 and 4
Done with Data having 3 and 4
[<_MainThread(MainThread, started 105612)>] App: Worker finished with Data having 3 and 4
[<_MainThread(MainThread, started 105612)>] Processing Data having 5 and 6
Done with Data having 5 and 6
[<_MainThread(MainThread, started 105612)>] App: Worker finished with Data having 5 and 6
[<_MainThread(MainThread, started 105612)>] Tasks sent

我做错了什么?不幸的是,关于此问题的PyQt文档非常不完整且相互矛盾。

如果我使用moveToThread技术,我会得到类似的结果:

from PyQt4 import QtGui, QtCore
import threading
from time import sleep
import sys

class Data():
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __str__(self):
        return "Data having %d and %d" % (self.a, self.b)

class Worker(QtCore.QObject):
    def __init__(self, parent):
        QtCore.QObject.__init__(self)
        self.connect(parent, QtCore.SIGNAL("newTask"), self.task)

    def task(self, dataobj):
        print "[%s] Processing" % threading.currentThread(), dataobj
        sleep(3)
        print "Done with", dataobj
        self.emit(QtCore.SIGNAL("taskDone"), str(dataobj))

    def start(self):
        print "[%s] start()" % threading.currentThread()

class App(QtCore.QObject):
    def __init__(self):
        QtCore.QObject.__init__(self)

        self.w = Worker(self)
        self.t = QtCore.QThread(self)
        self.w.moveToThread(self.t)
        self.connect(self.w, QtCore.SIGNAL("taskDone"), self.on_task_done)
        self.connect(self.t, QtCore.SIGNAL("started()"), self.w.start)
        self.t.start()

    def assign_tasks(self):
        self.emit(QtCore.SIGNAL("newTask"), Data(3, 4))
        self.emit(QtCore.SIGNAL("newTask"), Data(5, 6))
        print "[%s] Tasks sent" % threading.currentThread()

    @staticmethod
    def on_task_done(objstr):
        print "[%s] App: Worker finished with" % threading.currentThread(), objstr

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    a = App()
    sleep(1)
    a.assign_tasks()
    sleep(20)
    sys.exit(app.exec_())

这导致以下结果:
[<_DummyThread(Dummy-1, started daemon 108992)>] start()
[<_MainThread(MainThread, started 107004)>] Processing Data having 3 and 4
Done with Data having 3 and 4
[<_MainThread(MainThread, started 107004)>] App: Worker finished with Data having 3 and 4
[<_MainThread(MainThread, started 107004)>] Processing Data having 5 and 6
Done with Data having 5 and 6
[<_MainThread(MainThread, started 107004)>] App: Worker finished with Data having 5 and 6
[<_MainThread(MainThread, started 107004)>] Tasks sent

我设法让第二个模式运行起来了,这个模式使用了moveToThread,通过在Worker.start()中连接信号而不是在Worker.init()中连接信号。 start()是工作线程中第一件运行的事情。 我仍然想知道为什么第一个模式不起作用... 我知道“新”的方法不同,但是发生了什么? - jpcgt
1个回答

10

你的Worker对象“驻扎”在主线程中,这意味着它们的所有信号都将由主线程的事件循环处理。这些对象是 QThread不会改变这个事实。

如果你想让信号被另一个线程处理,你需要使用 moveToThread 方法将工作对象移动到该线程。

所以,在你的情况下,只有 run 方法实际上在不同的线程中执行, task 方法仍然在主线程中执行。更改此方式的一种方法是:

  • 使你的 Worker 成为常规的 QObject,而不是 QThread
  • 在你的 App 中创建一个 QThread,启动并将工作者移动到该线程
  • 然后发送信号给工作者,导致它开始处理

并且你应该查看这些参考资料:


编辑:

我在你的代码中注意到了一些其他的事情:

  • 你混合使用 python 的线程和 Qt 的线程。 threading.currentThread 不会正确地反映当前的 Qt 线程,应该使用 QThread.currentThread()
  • 对你调用的 slot 进行修饰 pyqtSlots,不这样做可能会导致类似的问题。
  • 使用新风格信号。PyQt5 不再支持旧风格信号,而新风格信号更易于使用。

所以,这是一个可以正常工作的版本:

from PyQt4 import QtGui, QtCore
import threading
from time import sleep
import sys

class Data():
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def __str__(self):
        return "Data having %d and %d" % (self.a, self.b)

class Worker(QtCore.QObject):

    taskDone = QtCore.pyqtSignal(str)

    def __init__(self, parent):
        QtCore.QObject.__init__(self)
        parent.newTask.connect(self.task)

    @QtCore.pyqtSlot(object)
    def task(self, dataobj):
        print "[%s] Processing" % QtCore.QThread.currentThread().objectName(), dataobj
        sleep(3)
        print "Done with", dataobj
        self.taskDone.emit(str(dataobj))

    @QtCore.pyqtSlot()
    def start(self):
        print "[%s] start()" % QtCore.QThread.currentThread().objectName()

class App(QtCore.QObject):

    newTask = QtCore.pyqtSignal(object)

    def __init__(self):
        QtCore.QObject.__init__(self)
        self.w = Worker(self)
        self.t = QtCore.QThread(self, objectName='workerThread')
        self.w.moveToThread(self.t)
        self.w.taskDone.connect(self.on_task_done)
        self.t.started.connect(self.w.start)
        self.t.start()

    def assign_tasks(self):
        self.newTask.emit(Data(3, 4))
        self.newTask.emit(Data(5, 6))
        print "[%s] Tasks sent" % QtCore.QThread.currentThread().objectName()

    @staticmethod
    def on_task_done(objstr):
        print "[%s] App: Worker finished with" % QtCore.QThread.currentThread().objectName(), objstr

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    QtCore.QThread.currentThread().setObjectName('main')
    a = App()
    sleep(1)
    a.assign_tasks()
    from utils import sigint
    sys.exit(app.exec_())

为了使输出更易读,我已经设置了线程的 objectName

[workerThread] start() // [工作线程] 启动
[main] Tasks sent // [主线程] 发送任务
[workerThread] Processing Data having 3 and 4 // [工作线程] 处理数据 3 和 4
Done with Data having 3 and 4 // 完成数据 3 和 4 的处理
[workerThread] Processing Data having 5 and 6 // [工作线程] 处理数据 5 和 6
[main] App: Worker finished with Data having 3 and 4 // [应用程序主线程] 工作线程完成了数据 3 和 4 的处理
Done with Data having 5 and 6 // 完成数据 5 和 6 的处理
[main] App: Worker finished with Data having 5 and 6 // [应用程序主线程] 工作线程完成了数据 5 和 6 的处理

我尝试了 moveToThread 模式,但结果类似。在所示的示例中,只有 run() 在工作线程中运行,但使用 moveToThread 模式时,只有连接到线程对象的 started() 信号的内容在单独的线程中运行。我无法让对象响应从应用程序发送的信号。也许我必须在 run() 中启动某种事件监听器?谢谢。 - jpcgt
感谢您提供详细的答案。这看起来是正确的做法。我正在将我的应用程序从Gtk+3迁移到PyQt4以获得稳定性,但是使用单独的线程和工作对象的线程使用模式让我感到困惑。似乎我为相同的事情使用了两倍的代码。这个moveToThread的东西以及在线程、工作者和主线程之间连接信号如何帮助?再次感谢! - jpcgt

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