Emitting signals from a Python thread using QObject
Asked Answered
C

2

8

I would like to know what are the consequences of emitting a signal from a regular python thread within a QObject, compared with a QThread.

See the following class:

class MyObject(QtCore.QObject):

    def __init__(self):
        super().__init__()

    sig = pyqtSignal()

    def start(self):
        self._thread = Thread(target=self.run)
        self._thread.start()

    def run(self):
        self.sig.emit()
        # Do something

Now, assuming that in the GUI thread, I have:

def __init__(self):
    self.obj = MyObject()
    self.obj.sig.connect(self.slot)
    self.obj.start()

def slot(self):
    # Do something

the slot is indeed executed when the signal is emitted. However, I would like to know which thread will the slot method be executed in? Would it be any different if I used a QThread instead of a python thread in MyObject?

I am using PyQt5 and Python 3.

Cru answered 5/6, 2015 at 22:16 Comment(0)
N
11

By default, Qt automatically queues signals when they are emitted across threads. To do this, it serializes the signal parameters and then posts an event to the event-queue of the receiving thread, where any connected slots will eventually be executed. Signals emitted in this way are therefore guaranteed to be thread-safe.

With regard to external threads, the Qt docs state the following:

Note: Qt's threading classes are implemented with native threading APIs; e.g., Win32 and pthreads. Therefore, they can be used with threads of the same native API.

In general, if the docs state that a Qt API is thread-safe, that guarantee applies to all threads that were created using the same native library - not just the ones that were created by Qt itself. This means it is also safe to explicitly post events to other threads using such thread-safe APIs as postEvent() and invoke().

There is therefore no real difference between using threading.Thread and QThread when it comes to emitting cross-thread signals, so long as both Python and Qt use the same underlying native threading library. This suggests that one possible reason to prefer using QThread in a PyQt application is portability, since there will then be no danger of mixing incompatible threading implementations. However, it is highly unlikely that this issue will ever arise in practice, given that both Python and Qt are deliberately designed to be cross-platform.


As to the question of which thread the slot will be executed in - for both Python and Qt, it will be in the main thread. By contrast, the run method will be executed in the worker thread. This is a very important consideration when doing multi-threading in a Qt application, because it is not safe to perform gui operations outside the main thread. Using signals allows you to safely communicate between the worker thread and the gui, because the slot connected to the signal emitted from the worker will be called in the main thread, allowing you to update the gui there if necessary.

Below is a simple script that shows which thread each method is called in:

import sys, time, threading
from PyQt5 import QtCore, QtWidgets

def thread_info(msg):
    print(msg, int(QtCore.QThread.currentThreadId()),
          threading.current_thread().name)

class PyThreadObject(QtCore.QObject):
    sig = QtCore.pyqtSignal()

    def start(self):
        self._thread = threading.Thread(target=self.run)
        self._thread.start()

    def run(self):
        time.sleep(1)
        thread_info('py:run')
        self.sig.emit()

class QtThreadObject(QtCore.QThread):
    sig = QtCore.pyqtSignal()

    def run(self):
        time.sleep(1)
        thread_info('qt:run')
        self.sig.emit()

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.pyobj = PyThreadObject()
        self.pyobj.sig.connect(self.pyslot)
        self.pyobj.start()
        self.qtobj = QtThreadObject()
        self.qtobj.sig.connect(self.qtslot)
        self.qtobj.start()

    def pyslot(self):
        thread_info('py:slot')

    def qtslot(self):
        thread_info('qt:slot')

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 100, 300, 200)
    window.show()
    thread_info('main')
    sys.exit(app.exec_())

Output:

main 140300376593728 MainThread
py:run 140299947104000 Thread-1
py:slot 140300376593728 MainThread
qt:run 140299871450880 Dummy-2
qt:slot 140300376593728 MainThread
Noella answered 12/4, 2018 at 17:22 Comment(3)
from your link: "you can't use Qt from a Python thread (you can't for instance post event to the main thread through QApplication.postEvent): you need a QThread for that to work" (emit() calls postEvent() and therefore if we would believe the post you've linked, we can't .emit() from a Python thread.Digestion
@jfs. If you read the comments on that answer, you will see that that claim is just plain wrong. There is no evidence whatsoever to support it, so you can safely ignore it.Noella
@jfs. Since it was old and somewhat controversial, I have removed the link from my answer and added some completely new material. I would have liked to have found a more complete official Qt statement regarding external threads, but I think the one I found is pretty clear.Noella
S
0

I would like to add:

class MyQThread(QThread):
    signal = pyqtSignal() # This thread emits this at some point.

class MainThreadObject(QObject):
    def __init__(self):
        thread = MyQThread()
        thread.signal.connect(self.mainThreadSlot)
        thread.start()

    @pyqtSlot()
    def mainThreadSlot(self):
        pass

This is perfectly OK, according to all documentation I know of. As is the following:

class MyQObject(QObject):
    signal = pyqtSignal()

class MainThreadObject(QObject):
    def __init__(self):
        self.obj = MyQObject()
        self.obj.signal.connect(self.mainThreadSlot)
        self.thread = threading.Thread(target=self.callback)
        self.thread.start()

    def callback(self):
        self.obj.signal.emit()

    @pyqtSlot()
    def mainThreadSlot(self):
        pass

From what @ekhumoro is saying, those two are functionally the same thing. Because a QThread is just a QObject who's run() method is the target= of a threading.Thread.

In other words, both the MyQThread's and the MyQObject's signal is memory "owned" by the main thread, but accessed from child threads.

Therefore the following should also be safe:

class MainThreadObject(QObject):
    signal = pyqtSignal() # Connect to this signal from QML or Python

    def __init__(self):
        self.thread = threading.Thread(target=self.callback)
        self.thread.start()

    def callback(self):
        self.signal.emit()

Please correct me if I am wrong. It would be very nice to have official documentation on this behavior from Qt and/or Riverbank.

Stanwood answered 6/12, 2018 at 10:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.