I have a data acquisition thread which samples and processes data which it then emits as a signal to a receiver.
Now, when that thread is stopped, how can I ensure it has finished the current loop and emitted its signal before proceeding (and e.g. emitting a summary signal)?
import sys
import time
from PySide6.QtCore import Signal, Slot
from PySide6 import QtCore
from PySide6 import QtWidgets
##==============================================================================
class EmitterClassThreaded(QtCore.QThread):
## Define a signal that emits a dictionary
data_signal = Signal(dict)
##--------------------------------------------------------------------------
def __init__(self):
super().__init__()
self.counter = 0
self.t_start = time.time()
self.running = True
## Connect the signal to a method within the same class
self.data_signal.connect(self.handle_data)
##--------------------------------------------------------------------------
def run(self):
while self.running:
self.counter += 1
now = time.time() - self.t_start
data = {'counter': self.counter, 'timestamp': f"{now:.1f}"}
time.sleep(1) # <------ doing something here which takes time
self.data_signal.emit(data)
##--------------------------------------------------------------------------
def stop(self):
self.running = False
##--------------------------------------------------------------------------
@Slot(dict)
def handle_data(self, data):
print(f"EmitterClassThreaded received data: {data}")
##==============================================================================
class ReceiverClass():
def __init__(self):
super().__init__()
##--------------------------------------------------------------------------
@Slot(dict)
def handle_data(self, data):
print(f"ReceiverClass received data: {data}")
##==============================================================================
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Example ThreadedEmitter-Receiver")
self.setGeometry(100, 100, 400, 200)
self.label = QtWidgets.QLabel("Waiting for signal...", self)
self.label.move(150, 80)
self.stop_button = QtWidgets.QPushButton("Stop Emitter", self)
self.stop_button.move(150, 120)
self.stop_button.clicked.connect(self.stop_emitter)
self.emitter = EmitterClassThreaded()
self.emitter.data_signal.connect(self.handle_data)
self.receiver = ReceiverClass()
## Connect the signal from EmitterClass to the method in ReceiverClass
self.emitter.data_signal.connect(self.receiver.handle_data)
## Start the emitter thread
self.emitter.start()
self.emitter.running = True
##--------------------------------------------------------------------------
@Slot(dict)
def handle_data(self, data):
self.label.setText(f"Counter: {data['counter']}\nTimestamp: {data['timestamp']}")
##--------------------------------------------------------------------------
def stop_emitter(self):
print("ReceiverClass: Stopping the emitter thread...")
self.emitter.stop()
## TODO: Wait for the thread to finish (incl. emitting the last signal) before proceeding
print("Creating own data to emit.")
self.emitter.data_signal.emit({'counter': -999, 'timestamp': 0})
##******************************************************************************
##******************************************************************************
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
In my current example, the last signal from the thread always overwrites that summary signal. Thanks in advance!
The problem is caused by the fact that you're emitting the signal from another object, and, more specifically, from another thread.
In general, it's normally preferred to emit signals directly from "within" their object, and emitting them externally is generally discouraged (but not forbidden nor completely wrong in principle).
Note, though, that it's also important to be aware of the thread from which the signal is emitted.
For instance, trying to do the following will not solve the problem:
class EmitterClassThreaded(QtCore.QThread):
...
def stop(self):
self.running = False
self.data_signal.emit({'counter': -999, 'timestamp': 0})
That code won't change anything, because stop()
is being directly called from the main thread, and the fact that stop()
is a member of the QThread instance is irrelevant.
Remember that QThreads are objects that manage execution OS threads, they are not "the thread": directly calling someMethod()
on a QThread instance will not cause that method to be executed in the related thread.
As you correctly assumed, when you emit the signal from the main thread, the other thread is still running (doing whatever you simulated with time.sleep()
), therefore a further signal from the thread will be emitted afterwards.
Depending on the situations, many alternatives exist.
finished
signalThe simpler solution is to make use of the finished
signal that QThread provides. That signal is always emitted once the thread is finished, even when not using QThread's own event loop by overriding run()
.
A possible approach, then, could be this:
class EmitterClassThreaded(QtCore.QThread):
data_signal = Signal(dict)
def __init__(self):
...
self.finished.connect(self.emitFinal)
def emitFinal(self):
self.data_signal.emit({'counter': -999, 'timestamp': 0})
...
You could change the logic of the while loop by checking the running
flag before trying to emit the signal:
def run(self):
while True:
self.counter += 1
now = time.time() - self.t_start
data = {'counter': self.counter, 'timestamp': "{:.1f}".format(now)}
time.sleep(1) # <------ doing something here which takes time
if self.running:
self.data_signal.emit(data)
else:
self.data_signal.emit({'counter': -999, 'timestamp': 0})
break
In case you still need the last computed data, you may emit that in any case, and eventually exit the loop after emitting the "final" signal:
...
self.data_signal.emit(data)
if not self.running:
self.data_signal.emit({'counter': -999, 'timestamp': 0})
break
Alternatively, since self.counter
is a simple reference to an integer (and, therefore, thread safe), you may even change the above just by checking the self.counter
value to -999
and check whether self.counter < 0
:
class EmitterClassThreaded(QtCore.QThread):
data_signal = Signal(dict)
def __init__(self):
super().__init__()
self.counter = 0
self.t_start = time.time()
# no self.running
def run(self):
while True:
self.counter += 1
now = time.time() - self.t_start
data = {'counter': self.counter, 'timestamp': "{:.1f}".format(now)}
time.sleep(1) # <------ doing something here which takes time
self.data_signal.emit(data)
if self.counter < 0:
self.data_signal.emit({'counter': -999, 'timestamp': 0})
break
def stop(self):
self.counter = -999
...
wait()
after trying to "stop" the threadIn some cases, it may be necessary to completely block everything until the thread is done, which can be achieved by using QThread.wait()
.
Note that the documentation says that it "Blocks the thread until [...]". In this case "the thread" is the calling thread; consider the following change:
def stop_emitter(self):
print("ReceiverClass: Stopping the emitter thread...")
self.emitter.stop()
self.emitter.wait()
print("Creating own data to emit.")
self.emitter.data_signal.emit({'counter': -999, 'timestamp': 0})
This is perfectly valid, in theory, because it works similarly to Python's thread.join()
. Unfortunately, it's also discouraged in a case like this, because its blocking nature means that calling it in the main thread will block the main event loop, resulting in UI freeze until the thread has finished.
A possible alternative would be to use wait()
with a small interval and ensure that the main app processEvents()
is called:
def stop_emitter(self):
print("ReceiverClass: Stopping the emitter thread...")
self.emitter.stop()
while not self.emitter.wait(10):
QApplication.processEvents()
QApplication.processEvents()
print("Creating own data to emit.")
self.emitter.data_signal.emit({'counter': -999, 'timestamp': 0})
Note that the further call to processEvents
outside of the loop is necessary, because there will still be pending events, most importantly, the last signal from the exiting thread (which has been queued).
terminate()
(no, don't)Unlike Python, QThread provides the terminate()
function, so you could add self.emitter.terminate()
right after self.emitter.stop()
.
In reality, killing threads is considered a bad pattern, and highly discouraged in any language.
You may try to use it, at your own risk, but only IF you have deep understanding of how threading works in all involved systems and possible permutations (including hardware and software aspects), and full awareness of the objects used in the actual execution within the thread.
That's a huge "if": if you're here because you're asking yourself if you could use terminate()
, then it most certainly means that you should not, because that's one choice you can only take if your experiences tell you that you are fully aware that it is the case of using it (and if you are, you probably wouldn't be reading this while looking for suggestions).
So: no, do not use terminate()
.
It's important to note that threads have important limitations, especially when dealing with Python (see Global Interpreter Lock).
Simply put, if whatever is done within the run()
override (or any function connected to the started
signal of the thread) is purely CPU bound, there is fundamentally no benefit in using threading: it just introduces further complications, and does not provide actual concurrency.
The only cases in which threading makes sense is when using IO (eg: file read/write, network access, etc.) or calls that do not require waiting for their results (but still need to be executed in separate threads).
If what you do within run()
is a long and heavy computation, then you only have two options:
for
loop), then ensure that sleep
calls are frequently added at small intervals;Note that, in the first case, this can still be achieved without using threading (just replace sleep
with QApplication.processEvents()
).
To clarify, consider the following example:
class EmitterClassThreaded(QtCore.QThread):
...
def run(self):
while self.running:
self.counter += 1
now = time.time() - self.t_start
data = {'counter': self.counter, 'timestamp': f"{now:.1f}"}
for i in range(1000):
# some relatively long and complex computation
time.sleep(.01) # temporarily release control to other threads
self.data_signal.emit(data)
In this case, while perfectly reasonable, you're not actually using advantages threading could provide, it's just a different code structure that "coincidentally uses" threading, but without real benefit.
The same could be achieved with a simple class (without threading), that calls QApplication.processEvents()
instead of time.sleep(.01)
. If properly written, it could even be more efficient, because it wouldn't need to always wait that interval if the main event loop doesn't have queued events that require processing.
The PySide Slot
, Property
and Signal
decorators (along with the pyqt*
prefix based decorators in PyQt) only make sense for QObject based classes.
Those decorators should only be used in QObject subclasses (including Python object
subclasses used in mix-ins with QObject based ones), otherwise they are completely useless and should be avoided to begin with:
Slot
(or pyqtSlot
) decorator is rarely necessary, as it almost always provides very little benefits, and is only required in very specific cases (dealing with complex threading based scenarios, or when implementing QtDesigner plugins);Property
(or pyqtProperty
) decorator behaves like a Python property: if the class is never used as/within a QObject one, then just use @property
;emit
ted;See this related post for further details: Why do I need to decorate connected slots with pyqtSlot?.