[PyQt] moveToThread and signals

David Morris othalan at othalan.net
Wed Jul 3 22:58:17 BST 2019


I have just observed some odd behavior and would like to know if this is
normal or a bug.

I have a non-Qt thread where I create a Qt object, connect a signal, move
the object to the main application thread, then use the signal to execute a
callback function within the Qt event loop.  Here is some sample code that
can be copied directly into a python instance:

# Create the Qt application in a different thread
# so that testing is easy from the python command prompt
class QtAppThread(threading.Thread):
    def run(self):
        from PyQt5.QtCore import QThread,QCoreApplication
        self.app = QCoreApplication([])
        print(f"Main App Thread Id: {int(QThread.currentThreadId())}")
        self.app.exec_()
appthread = QtAppThread()
appthread.start()

# Run from a thread that is NOT a QThread:
from PyQt5.QtCore import *
class QSigRunner(QObject):
    sig_run = pyqtSignal(object, tuple)

    def __init__(self, *arg, **kw):
        super().__init__(*arg, **kw)
        self.sig_run.connect(self.on_sig_run)

    def on_sig_run(self, func, args):
        func(*args)

def my_callback(*args):
    print(f"Called with {args}")
    print(f"Thread {int(QThread.currentThreadId())}")

obj = QSigRunner()
obj.moveToThread(QCoreApplication.instance().thread())
print(f"My Thread Id: {int(QThread.currentThreadId())}")
obj.sig_run.emit(my_callback, (True,))


Based on the description of pyqt/qt signals, I expected the above code to
execute in the main application's thread, however examining thread IDs I
see that the callback function is executed in the same thread as I emitted
the signal.

However, if I change the above code in a very minor way to declare the
method on_sig_run as an explicity pyqtSlot, everything works as expected
and the emitted signal is run as part of the main Qt thread via the Qt
event loop.  Here is the code:

# Run from a thread that is NOT a QThread:
from PyQt5.QtCore import *
class QSigRunner(QObject):
    sig_run = pyqtSignal(object, tuple)

    def __init__(self, *arg, **kw):
        super().__init__(*arg, **kw)
        self.sig_run.connect(self.on_sig_run)

    @pyqtSlot(object, tuple)
    def on_sig_run(self, func, args):
        func(*args)

def my_callback(*args):
    print(f"Called with {args}")
    print(f"Thread {int(QThread.currentThreadId())}")

obj = QSigRunner()
obj.moveToThread(QCoreApplication.instance().thread())
print(f"My Thread Id: {int(QThread.currentThreadId())}")
obj.sig_run.emit(my_callback, (True,))


Alternatively, I can connect the signal *after* moving the object to a new
thread and again, everything works perfectly with the emitted signal
executing in the Qt event loop.  Here is the updated code:

# Run from a thread that is NOT a QThread:
from PyQt5.QtCore import *
class QSigRunner(QObject):
    sig_run = pyqtSignal(object, tuple)

    def __init__(self, *arg, **kw):
        super().__init__(*arg, **kw)

    def on_sig_run(self, func, args):
        func(*args)

    def moveToThread(self, *arg, **kw):
        super().moveToThread(*arg, **kw)
        self.sig_run.connect(self.on_sig_run)

def my_callback(*args):
    print(f"Called with {args}")
    print(f"Thread {int(QThread.currentThreadId())}")

obj = QSigRunner()
obj.moveToThread(QCoreApplication.instance().thread())
print(f"My Thread Id: {int(QThread.currentThreadId())}")
obj.sig_run.emit(my_callback, (True,))


Is this acting as expected?  If so, can anyone explain why the first
version does not work but the other two both work?  Or is there a bug
involved in the first configuration?

Thank you,
David
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20190704/497f156a/attachment-0001.html>


More information about the PyQt mailing list