pythonpython-2.7pyqt5qthreadqtimer

How to use a QTimer in a separate QThread


I have some computationally heavy task that I want to run in a loop after every 5 seconds without blocking the main event-loop. For this, I intend to use a QTimer and a separate thread to run it. I have tried the following code but it has not worked so far:

@pyqtSlot()
def heavy_task_function():
    # Sleep for 10 seconds to simulate heavy computation
    time.sleep(10)
    print "First Timer Fired"

if __name__ == "__main__":
    app = QCoreApplication.instance()
    if app is None:
        app = QApplication(sys.argv)

    threaded_timer = ModbusComThread(heavy_task_function)
    threaded_timer.start()

    sys.exit(app.exec_())

Where:

class ModbusComThread(QThread):

    def __init__(self, slot_function):
        QThread.__init__(self)
        self.slot_function = slot_function
        self.send_data_timer = None

    def run(self):
        print "Timer started on different thread"
        self.send_data_timer = QTimer(self)
        self.send_data_timer.timeout.connect(self.slot_function)
        self.send_data_timer.start(5000)

    def stop(self):
        self.send_data_timer.stop()

The slot_function is never fired by the QTimer in threaded_timer. Is my threading architecture correct?


Solution

  • A QTimer needs a running event-loop. By default, QThread.run() will start a local event-loop for the thread, but if you completely override it in the way that you have done, that won't happen - so the timer events will never be processed.

    In general, when you need a local event-loop you should create a worker object to do all the processing and then use moveToThread to put it in a separate thread. If not, it's perfectly okay to override QThread.run().

    The demo below shows how to do this. Note that it's very important to create the timer after the thread has started, otherwise it would be created in the wrong thread and its timer-events wouldn't be processed by the thread's event-loop. It's also important that all communication between the worker thread and the main thread is done via signals, so as to ensure thread-safety. Never try to directly perform GUI operations outside the main thread, as Qt does not support that at all. For the purposes of the demo, a second timer in the main thread is used to stop all processing after a fixed interval. If there was a GUI, user intervention via a button would achieve the same thing.

    Demo:

    import sys, threading
    from PyQt5.QtCore import (
        QCoreApplication, QTimer, QThread, QObject, pyqtSignal,
        )
    # from PyQt6.QtCore import (
    #     QCoreApplication, QTimer, QThread, QObject, pyqtSignal,
    #     )
    # from PySide6.QtCore import (
    #     QCoreApplication, QTimer, QThread, QObject, Signal as pyqtSignal,
    #     )
    
    class ModbusComWorker(QObject):
        finished = pyqtSignal()
    
        def start(self):
            self._timer = QTimer(self)
            self._timer.timeout.connect(self.process)
            self._timer.start(2000)
    
        def stop(self):
            self._timer.stop()
            self.finished.emit()
    
        def process(self):
            print('processing (thread: %r)' % threading.current_thread().name)
            QThread.sleep(3)
    
    if __name__ == "__main__":
    
        app = QCoreApplication.instance()
        if app is None:
            app = QCoreApplication(sys.argv)
    
        thread = QThread()
        worker = ModbusComWorker()
        worker.moveToThread(thread)
    
        def finish():
            print('shutting down...')
            thread.quit()
            thread.wait()
            app.quit()
            print('stopped')
    
        worker.finished.connect(finish)
        thread.started.connect(worker.start)
        thread.start()
    
        timer = QTimer()
        timer.setSingleShot(True)
        timer.timeout.connect(worker.stop)
        timer.start(15000)
    
        print('processing (thread: %r)' % threading.current_thread().name)
    
        sys.exit(app.exec())
    

    Output:

    starting (thread: <PyQt5.QtCore.QThread object at 0x7f980d096b98>)
    processing (thread: <PyQt5.QtCore.QThread object at 0x7f980d0968a0>)
    processing (thread: <PyQt5.QtCore.QThread object at 0x7f980d0968a0>)
    processing (thread: <PyQt5.QtCore.QThread object at 0x7f980d0968a0>)
    processing (thread: <PyQt5.QtCore.QThread object at 0x7f980d0968a0>)
    processing (thread: <PyQt5.QtCore.QThread object at 0x7f980d0968a0>)
    shutting down...
    stopped