Unable to get QTimer to call slot from non-GUI thread.



  • For QTimer to work, you must have an event loop in your application; that is, you must call QCoreApplication::exec() somewhere. Timer events will be delivered only while the event loop is running. In multithreaded applications, you can use QTimer in any thread that has an event loop. To start an event loop from a non-GUI thread, use QThread::exec(). Qt uses the timer's thread affinity to determine which thread will emit thetimeout() signal. Because of this, you must start and stop the timer in its thread; it is not possible to start a timer from another thread.

    I have a main GUI thread where business objects are created. These business objects need timers to run because they are are Button (with timed press events), Timer, and Graph (which runs over time).

    I have a non-GUI thread called RunnerThread that calls the methods of the business objects which depend on QTimer. For instance:

    def play(self):
            if self._paused or self._timer is None: 
                if self._timer is None:
                    self._pauseTime = 0.0
                    self._loopCounter = 1
                else:
                    #self._timer.cancel()
                    self._timer.killTimer()
                self._paused = False
                self._elapsed = QElapsedTimer()
                self._timer = QTimer()
                self._timer.moveToThread(QThread.thread(self))
                self._timer.timeout.connect(self._displayOnLEDs)
                self._timer.setSingleShot(False)
                self._timer.setInterval(10)
                self._timer.start()
                #self._timer = threading.Timer(10/1000, self._displayOnLEDs)
                #self._startThread()
                self._time = 0
                self._elapsed.start()
    

    is the play() method of Graph which essentially gets called by the RunnerThread. This timer does not work (slot never gets called). I've tried all available solutions on Google that were applicable to my setup including called QCoreApplication.processEvents() and QThread.exec() within RunnerThread.

    I've also tried moving over to threading.Timer but this causes issues: too many repeated creations of a Timer will crash my app with "too many threads" type error even though I cancel() the previously created timer. So I cannot use threading.Timer.

    Please show me how to get QTimer to work when created / started on a non-GUI thread.

    Thanks.


  • Lifetime Qt Champion

    Hi,

    Please provide the full class implementation. With the function you wrote, it's really not clear what you are trying to implement.



  • Here is minimal running code which demonstrates my bug. "playing" will never be seen on the console:

    from PyQt5.QtCore import QThread, QCoreApplication, QTimer, QElapsedTimer, QObject, pyqtSignal, Qt
    from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
    import sys
    
    
    class RunnerThread(QThread):
        def __init__(self, obj, thread):
            super().__init__()
            self._thread = thread
            self._obj = obj
            
        def run(self):
            self._obj.play(self._thread)
            
    
    class MyObject(QObject):
        def __init__(self):
            super().__init__()
            
        def play(self, thread):
            self._timer = QTimer(self)
            self._timer.moveToThread(thread)
            self._timer.setInterval(300)
            self._timer.timeout.connect(self._play)
            self._timer.setSingleShot(True)
            self._timer.start()
            print("starting")
            
        def _play(self):
            print("playing")
            
    
    if __name__ == "__main__":
        app = QApplication([])
        
        window = QMainWindow()
        window.show()
        
        button = QPushButton("play")
        window.setCentralWidget(button)
        
        obj = MyObject()
        thread = QThread.currentThread()
        runner = RunnerThread(obj, thread)  
        button.clicked.connect(runner.start)
        
        sys.exit(app.exec_())
    

  • Lifetime Qt Champion

    Your obj instance doesn't belong to the correct thread.



  • @SGaist How do I fix that?


  • Lifetime Qt Champion

    I just realised something more, you are creating a new thread, where you store the current thread (main thread in this case) then in your play function you move the timer to the main thread.

    That's wrong in many ways.

    Don't call moveToThread like you do.



  • @SGaist That was only an attempt to remedy the problem. I wouldn't do that normally.

    I have an editor where the business objects are created in the gui thread, so how do I make their timers work on another thread?



  • This code seems to work. Solution: call exec() within QThread run() (which starts its own event loop and takes control of the thread), which means you can't have a main while loop in run(). I will research how to work with Qt event loops.

    from PyQt5.QtCore import QThread, QCoreApplication, QTimer, QElapsedTimer, QObject, pyqtSignal, Qt
    from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
    import sys
    
    
    class RunnerThread(QThread):
        def __init__(self, obj):
            super().__init__()
            self._obj = obj
    
            
        def run(self):
            self._obj.play()
            self.exec()
                
            
    
    class MyObject(QObject):
        def __init__(self):
            super().__init__()
            
        def play(self):
            self._timer = QTimer(self)
            self._timer.setInterval(300)
            self._timer.timeout.connect(self._play)
            self._timer.setSingleShot(True)
            self._timer.start()
            print("starting")
            
        def _play(self):
            print("playing")
            
    
    if __name__ == "__main__":
        app = QApplication([])
        
        window = QMainWindow()
        window.show()
        
        button = QPushButton("play")
        window.setCentralWidget(button)
        
        obj = MyObject()
        runner = RunnerThread(obj)
        button.clicked.connect(runner.start)
        
        sys.exit(app.exec_())
    


  • int QCoreApplication::exec()


    Enters the main event loop and waits until exit() is called. Returns the value that was set to exit() (which is 0 if exit() is called via quit()).
    It is necessary to call this function to start event handling. The main event loop receives events from the window system and dispatches these to the application widgets.
    To make your application perform idle processing (i.e. executing a special function whenever there are no pending events), use a QTimer with 0 timeout. More advanced idle processing schemes can be achieved using processEvents().

    So I'll use a Qtimer with 0 timeout for my own event loop code.


  • Lifetime Qt Champion

    I missed that you weren't calling exec in your QThread subclass.

    You should take a look again at the QThread documentation about the various possibilities of its use.

    What do you mean by "main while loop" ?


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.