Search code examples
pythonpyqtpysideqthread

Waiting for a PyQt/PySide.QtCore.QThread to finish before doing something


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!


Solution

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

    Use the finished signal

    The 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})
    
        ...
    

    Emit the signal only if the thread must continue

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

    Use wait() after trying to "stop" the thread

    In 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).

    Use 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().

    Define and understand what is the actual task done in the thread

    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:

    • if it is or can be "broken" into smaller parts (eg. a long for loop), then ensure that sleep calls are frequently added at small intervals;
    • use multiprocessing;

    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.

    Unrelated considerations about decorators

    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:

    • the 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);
    • the Property (or pyqtProperty) decorator behaves like a Python property: if the class is never used as/within a QObject one, then just use @property;
    • non QObject instances cannot have Qt signals, and cannot therefore be emitted;

    See this related post for further details: Why do I need to decorate connected slots with pyqtSlot?.