I've been wondering over the question: is there are correct (or standard) way to create QThreads, which make of use of bidirectional signals?
That is, QThreads capable of processing not only custom workers in another thread, but also capable of processing events and signals incoming from the main thread, and also, capable of emitting signals to the main thread, so both threads can exchange data between each other using a single model: qt signals.
Most examples I've found so far (in articles, documentation, even here on SO) make use of unidirectional signals only, emitting them from the QThread to the main thread, to safely call non thread-safe QObject methods (that should be called only in the main thread).
The reason I would like to know if there's a such way to use bidirectional connection, is for the safety of passing data from the main thread to the QThread, without the need of explicitely using QMutexes or similar kinds of locks in order to exchange data, which is something that the Qt5 documentation doesn't cover, as far as I know.
I'm not saying that using bidirectional signals will automatically solve the problem for thread-safety in all forms. Reading/writting in shared memory or files, for example, would still require the usage of threading locks. I would just like to know if there is some kind of documentation about this question, or if anyone here has invented a working way to perform it.
Thread-safe bidirectional signals are quite easy to implement without any special handling. In most cases, Qt will automatically detect cross-thread signals and post an event with the serialised signal parameters (if any) to the event-queue of the receiving thread. This mechanism is the main reason for using moveToThread
rather than a QThread sub-class: as explained in the Qt docs, "the default implementation [of QThread.run] simply calls exec()", which starts the local event-loop. So, unless your sub-class explicitly calls exec
, it won't process any of the events it might receive (including signal and timer events). Of course, there are many legimate use-cases where this doesn't matter (as you can appreciate from reading the article you already linked to) - so it's simply a matter of choosing the right Qt API for the job. In addition to thread-safe bidirerctional signals, Qt also provides an API for more directly invoking methods using the same underlying mechanism: QMetaObject.invokeMethod. Taken together, these should be more than adequate for most situations.
Below is a basic example that illustrates some of the main techniques. Hopefully the code will be mostly self-explanatory, since it's mainly a matter of connecting/invoking the signals and slots in the right way. But note that calling QCoreApplication.processEvents is required within the while-loop, since it would otherwise block the worker's event-loop processing. The debugging output shows which thread each slot is being invoked in:
import threading
from PyQt5 import QtCore, QtWidgets
def check_thread(text):
print(f'{text}: {threading.current_thread().name}')
class Worker(QtCore.QObject):
countChanged = QtCore.pyqtSignal(int)
def __init__(self):
super().__init__()
self._active = False
self._step = 1
def process(self):
check_thread('process')
count = 0
self._active = True
while self._active:
QtCore.QThread.msleep(500)
QtCore.QCoreApplication.processEvents()
count += self._step
self.countChanged.emit(count)
self._active = False
@QtCore.pyqtSlot()
def stop(self):
check_thread('stopping')
self._active = False
@QtCore.pyqtSlot(int)
def handleStepChanged(self, step):
check_thread('step-change')
self._step = step
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.buttonStart = QtWidgets.QPushButton('Start')
self.buttonStart.clicked.connect(self.startWorker)
self.buttonStop = QtWidgets.QPushButton('Stop')
self.buttonStop.clicked.connect(self.stopWorker)
self.buttonStop.setEnabled(False)
self.labelOutput = QtWidgets.QLabel()
self.spinbox = QtWidgets.QSpinBox()
self.spinbox.setRange(1, 10)
layout = QtWidgets.QGridLayout(self)
layout.addWidget(self.labelOutput, 0, 0, 1, 3)
layout.addWidget(QtWidgets.QLabel('Step:'), 1, 0)
layout.addWidget(self.spinbox, 1, 1)
layout.addWidget(self.buttonStart, 1, 2)
layout.addWidget(self.buttonStop, 1, 3)
self.thread = QtCore.QThread(self)
self.worker = Worker()
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.process)
self.spinbox.valueChanged.connect(self.worker.handleStepChanged)
self.worker.countChanged.connect(self.handleCountChanged)
self.handleCountChanged()
@QtCore.pyqtSlot(int)
def handleCountChanged(self, count=0):
check_thread('count-change')
self.labelOutput.setText(f'<p>Current Count: <b>{count}</b></p>')
def startWorker(self):
check_thread('start')
if not self.thread.isRunning():
self.thread.start()
self.buttonStart.setEnabled(False)
self.buttonStop.setEnabled(True)
def stopWorker(self):
check_thread('stop')
if self.thread.isRunning():
self.buttonStart.setEnabled(True)
self.buttonStop.setEnabled(False)
QtCore.QMetaObject.invokeMethod(self.worker, 'stop')
self.thread.quit()
self.thread.wait()
def closeEvent(self, event):
self.stopWorker()
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 200, 100)
window.show()
app.exec()