Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyQt | Signals not handled in QThread but in main thread

Tags:

pyqt

qthread

In this simple PyQt demo program, I emit signals from the main thread. In a worker thread I connect to them, but the signal handlers are run in the main thread:

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

But the result reveals that the callbacks are run in the main thread:

[<_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

What am I doing wrong? Unfortunately, the PyQt docs on this are very incomplete and contradicting.

I'm getting similar results if I use the moveToThread technique:

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

Which results in:

[<_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
like image 719
jpcgt Avatar asked Dec 26 '22 09:12

jpcgt


1 Answers

Your Worker objects 'live' in the main thread, that means all their signals will be handled by the main thread's event loop. The fact that these objects are QThreads doesn't change that.

If you want signals to be processed by a different thread, you first need to move the worker object to that thread by using it's moveToThread method.

So in your case, only the run method is acutally executed in a different thread, the task method is still executed in the main thread. A way of changing this would be:

  • make your Worker a regular QObject, not a QThread
  • create a QThread in your App, start it and move the worker to that thread
  • then send the signal to the worker that causes it to start processing

And you should check out these references:

  • QObject thread affinity
  • Qt threading basics
  • Multithreading technologies in Qt

edit:

A few other things I noticed in your code:

  • you're mixing python threading and qt threading. threading.currentThread will not correctly reflect the current qt thread. use QThread.currentThread() for that.
  • decorate the slots you're calling as pyqtSlots, not doing so can be a cause of problems like these.
  • use new style signals. Old style signals are no longer supported in PyQt5, and new style signals are much easier and nicer to use.

So here is a version of your code that should work:

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

I've set the thread's objectNames to make the output better readable:

[workerThread] start()
[main] Tasks sent
[workerThread] Processing Data having 3 and 4
Done with Data having 3 and 4
[workerThread] Processing Data having 5 and 6
[main] App: Worker finished with Data having 3 and 4
Done with Data having 5 and 6
[main] App: Worker finished with Data having 5 and 6
like image 68
mata Avatar answered Feb 16 '23 05:02

mata