pythonpyqtpyqt5qthreadqtimer

Is a QTimer in a background thread updated even if the main thread is busy?


Let's say I have a PyQt GUI application and a QTimer running in the background using a QThread. The QTimer is set to an interval of 1 second and is running infinitely.

Now the main thread gets busy with some external C library call and blocks for 1 minute. Will the QTimer continue to get triggered during that period and signal it?

I am in a conceptual stage so there is no code to show yet. I know it would make more sense to do the big call in a background thread itself.


Solution

  • In short words, yes, QTimer will update on the other thread, granted that you take care of the thread's lifespan yourself and quit it only at the most appropriate times. Also be careful about the QTimer's reference, so it is not collected by the python's garbage collector (you'll need to stop the QTimer at one point).

    Now, with a bit of code to illustrate a more practical example, and so you can test it yourself:

    from PySide2.QtWidgets import QApplication, QMainWindow
    from PySide2.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
    from PySide2.QtCore import QObject, QThread, QTimer, Signal
    
    class TimerWorker(QObject):
        timeout = Signal(int)
    
        def __init__(self):
            QObject.__init__(self)
            self.count = 0
    
        def run(self):
            self.timer = QTimer()
            self.timer.setInterval(250)
            self.timer.timeout.connect(self.process)
            self.timer.start()
    
        def finished(self):
            self.timer.stop()
    
        def process(self):
            self.count += 1
            self.timeout.emit(self.count)
    
    class Window(QMainWindow):
        def __init__(self):
            QMainWindow.__init__(self)
    
            # Widgets
            self.scene = QWidget()
            self.label = QLabel("Count: 0")
            self.busyBt = QPushButton("Sleep")
            self.busyBt.clicked.connect(self.becomeBusy)
    
            # Scene
            layout = QVBoxLayout()
            layout.addWidget(self.label)
            layout.addWidget(self.busyBt)
            self.scene.setLayout(layout)
            self.setCentralWidget(self.scene)
    
            # Threads
            self.thread = QThread()
            self.worker = TimerWorker()
            self.worker.moveToThread(self.thread)
            self.worker.timeout.connect(self.processCount)
            self.thread.started.connect(self.worker.run)
            self.thread.finished.connect(self.worker.finished)
            self.thread.start()
    
        def becomeBusy(self):
            timeout = 5
            print('Main Thread: Sleeping for %d seconds...' % (timeout))
            QThread.sleep(timeout)
            print('Main Thread: Awaken')
    
        def processCount(self, count):
            self.label.setText("Count: %d" % (count))
    
        def closeEvent(self, evt):
            self.thread.quit()
            self.thread.wait()
    
    if __name__ == '__main__':
        app = QApplication()
        win = Window()
        win.show()
        app.exec_()
    

    In this example, we start a QThread which executes TimerWorker.run() on the other thread (using the moveToThread method). At each 0.25 seconds, it emits a signal that is scheduled on the main thread, to update the QLabel text.

    At the same time, the user can click a QPushButton to stop the main thread for 5 seconds. During that time, the application will freeze.


    By executing the script and clicking on the QPushButton to freeze the main thread, the QTimer keeps running and emitting the signal from the other thread. But as the main thread is sleeping (or busy), the signal is never processed. The signal will only be processed after the main thread awakes.

    So, if you need to keep the GUI always responsive for the Client (and always processing emitted signals), its recomended that you do all the heavy work on the QThread side, to not keep the application frozen for a time.