Search code examples
pythonpyqtpyqt5qthreadqtimer

QTimer on a Qthread


I have a GUI witch i need to update constantly using a Qtimer, for that I use a worker Qthread, this is my code :

from PyQt5.QtWidgets import QApplication, QPushButton, QWidget
from PyQt5.QtCore import QThread, QTimer
import sys
import threading


class WorkerThread(QThread):
    def run(self):
        print("thread started from :" + str(threading.get_ident()))
        timer = QTimer(self)
        timer.timeout.connect(self.work)
        timer.start(5000)
        self.exec_()

    def work(self):
        print("working from :" + str(threading.get_ident()))
        QThread.sleep(5)


class MyGui(QWidget):

    worker = WorkerThread()

    def __init__(self):
        super().__init__()
        self.initUi()
        print("Starting worker from :" + str(threading.get_ident()))
        self.worker.start()

    def initUi(self):
        self.setGeometry(500, 500, 300, 300)
        self.pb = QPushButton("Button", self)
        self.pb.move(50, 50)
        self.show()


app = QApplication(sys.argv)
gui = MyGui()
app.exec_()

the output is:

Starting worker from :824
thread started from :5916
working from :824
working from :824

the timer is working on the main thread witch freeze my Gui, How can i fix that ?


Solution

  • Answer: in your case I do not see the need to use QThread.

    TL; DR;

    When do I need to use another thread in the context of a GUI?

    Only one thread should be used when some task can block the main thread called the GUI thread, and the blocking is caused because the task is time-consuming, preventing the GUI eventloop from doing its job normally. All modern GUIs are executed in an eventloop that is allows you to receive notifications from the OS like the keyboard, the mouse, etc. and also allows you to modify the status of the GUI depending on the user.

    In your case I do not see any heavy task, so I do not see the need for a QThread, I do not really know what the task is that you want to run periodically.

    Assuming you have a task that consumes a lot of time, say 30 seconds and you have to do it every half hour, then the thread is necessary. and in your case you want to use a QTimer for it.

    Let's go in parts, QTimer is a class that inherits from a QObject, and a QObject belongs to the same as the parent, and if it does not have a parent it belongs to the thread where it was created. On the other hand many times it is thought that a QThread is a thread of Qt, but it is not, QThread is a class that allows to handle the life cycle of a native thread, and that is clearly stated in the docs: The QThread class provides a platform-independent way to manage threads.

    Knowing the above, let's analyze your code:

    timer = QTimer(self)
    

    In the above code self is the parent of QTimer and self is the QThread, so QTimer belongs to the thread of the parent of QThread or where QThread was created, not to the thread that QThread handles.

    Then let's see the code where QThread was created:

    worker = WorkerThread()
    

    As we see QThread has no parent, then QThread belongs to the thread where it was created, that is, QThread belongs to the main thread, and consequently its QTimer child also belongs to the main thread. Also note that the new thread that QThread handles only has the scope of the run() method , if the method is elsewhere belongs to the field where QThread was created, with all the above we see that the output of the code is correct, and the QThread.sleep(5) runs on the main thread causing the eventloop to crash and the GUI to freeze.

    So the solution is to remove the parent of QTimer so that the thread it belongs to is the one of the run() method, and move the work function within the same method. On the other hand it is a bad practice to create static attributes unnecessarily, considering the above the resulting code is the following:

    import sys
    import threading
    from PyQt5.QtCore import QThread, QTimer
    from PyQt5.QtWidgets import QApplication, QPushButton, QWidget
    
    
    class WorkerThread(QThread):
        def run(self):
            def work():
                print("working from :" + str(threading.get_ident()))
                QThread.sleep(5)
            print("thread started from :" + str(threading.get_ident()))
            timer = QTimer()
            timer.timeout.connect(work)
            timer.start(10000)
            self.exec_()
    
    class MyGui(QWidget):
        def __init__(self):
            super().__init__()
            self.initUi()
            self.worker = WorkerThread(self)
            print("Starting worker from :" + str(threading.get_ident()))
            self.worker.start()
    
        def initUi(self):
            self.setGeometry(500, 500, 300, 300)
            self.pb = QPushButton("Button", self)
            self.pb.move(50, 50)
    
    
    if __name__ == '__main__':    
        app = QApplication(sys.argv)
        gui = MyGui()
        gui.show()
        sys.exit(app.exec_())
    

    Output:

    Starting worker from :140068367037952
    thread started from :140067808999168
    working from :140067808999168
    working from :140067808999168
    

    Observations:

    • The heavy task that has been emulated is 5 seconds, and that task must be executed every 10 seconds. If your task takes longer than the period you should create other threads.

    • If your task is to perform a periodic task that is not as heavy as showing time then do not use new threads because you are adding complexity to a simple task, besides this may cause the debugging and testing stage to be more complex.