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:<
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.
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