3

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 ?

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
infantry
  • 336
  • 1
  • 5
  • 15

3 Answers3

5

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.

Community
  • 1
  • 1
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you for the quality answer and also for the valuable clarification, this really helped me, i'll opt for your answer. ps : this is just a straight to the point code for making it easier and understandable, the real work function has to periodically check the state of a bunch of servers and update the status in the Gui each time. – infantry Aug 28 '18 at 08:01
  • Do you mean to say "a QObject belongs to the same **thread** as the parent" instead of "a QObject belongs to the same as the parent"? That is, if a QObject is given a parent, then it exists in the parent thread, otherwise it exists within the thread it was created? – Lorem Ipsum Jan 01 '21 at 22:54
0

Sorry, I have misunderstood the question. It might be that this answer on another question might help you. The main message is that you should use the main event loop in Qt to not freeze the GUI instead of executing the thread on __init__: Pyqt5 qthread + signal not working + gui freeze

You can do that by using Qt slots with the decorator @pyqtSlot().

------------ old (wrong) answer ---------

QTimer can already work on separate threads, so I think you might be able to do it without writing that part yourself. For example you could do what you already do in a function:

 def update_gui(self):
     # edit: here is where you can add your gui update code:
     self.setGeometry(500, 500, 300, 300)
     self.pb = QPushButton("Button", self)
     self.pb.move(50, 50)
     self.show()
     # /edit (this is obviously only the setup code, but you get the idea)

     self.update_timer = QTimer()
     self.update_timer.setInterval(int(5000))
     self.update_timer.timeout.connect(self.update_gui)
     self.update_timer.start()

and call it in __init__. That's how I implemented some textbox that clears itself after a couple of seconds.

meetaig
  • 913
  • 10
  • 26
  • srry I'm just starting I couldn't follow you, wouldn't the timer creates more timers ?!, where is the work part ? – infantry Aug 27 '18 at 09:40
  • I did not add that part. But you can see that the function is called by the timer and then the timer sets itself up again, right? With this you can just add your work part before setting up the timer. – meetaig Aug 27 '18 at 09:42
  • the working part will execute on the main thread, that will make the Gui freeze – infantry Aug 27 '18 at 09:51
  • Oh ok, then I have misunderstood what you want to do. You want to spawn a thread that only ever updates every so often. Sorry! – meetaig Aug 27 '18 at 09:53
-1

Try it:

import sys
import threading
from PyQt5.QtGui     import *
from PyQt5.QtCore    import *
from PyQt5.QtWidgets import *

class WorkerThread(QThread):

    workSignal = pyqtSignal(str)

    def run(self):
        print("thread started from :" + str(threading.get_ident()))
        textLabel = "thread started from :" + str(threading.get_ident())
        self.workSignal.emit(textLabel)
        self.work()

    def work(self):
        print("working from :" + str(threading.get_ident()))
        textLabel = "working from :" + str(threading.get_ident())
        self.workSignal.emit(textLabel)


class MyGui(QWidget):

    worker = WorkerThread()

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

        self.worker.workSignal.connect(self.showLabel)

    def initUi(self):
        self.setGeometry(700, 350, 300, 150)

        self.lcdTime = QLCDNumber(self)
        self.lcdTime.setSegmentStyle(QLCDNumber.Filled)   # Outline Filled Flat
        self.lcdTime.setDigitCount(8)    

        self.timer = QTimer(self)
        self.lbl   = QLabel(self) 
        self.pb = QPushButton("Button Close", self, clicked=self.close)

        vbox = QVBoxLayout()
        vbox.addWidget(self.lcdTime)
        vbox.addWidget(self.lbl)
        vbox.addWidget(self.pb)
        self.setLayout(vbox)

        self.timer.timeout.connect(self.showTime)
        self.timer.start(1000)        
        self.numSec = 0
        self.show()

    def showTime(self):
        time = QTime.currentTime()
        text = time.toString("hh:mm:ss")           
        if ((time.second() % 2) == 0):
            text = text[0:2] + ' ' + text[3:5] + ' ' + text[6:]
        self.lcdTime.display(text)
        self.numSec += 1
        if self.numSec == 5:
            self.worker.start()
            self.numSec = 0

    def showLabel(self, textLabel):
        self.lbl.setText(textLabel)

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

enter image description here

S. Nick
  • 12,879
  • 8
  • 25
  • 33
  • So you basically moved the Qtimer from the worker thread to the main thread so now instead of calling the work function i just start the worker to work, humm this will do the trick :) thanks a lot. – infantry Aug 27 '18 at 12:32
  • @infantry that is not the answer to your question, your question clearly is that the timer lives in another thread, and the code that is shown does not do it, the timer is living in the thread of the GUI. – eyllanesc Aug 27 '18 at 15:35
  • @eyllanesc I totaly agree with you, the fact that the timer work on the main thread even it was created and started on the separated thread still ambiguous to me, but hey since i move only the Qtimer to the Gui thread it won't affect performance and Im still able to periodically call a worker that work on a separated thread (this is what i wanted to achieve, it's just another way to do it). – infantry Aug 27 '18 at 22:44
  • @infantry then change the title and content of your question, it clearly does not match the answer. That can bring confusion to other users. That code is stinky solution, has serious problems of how a thread is executed and what is its scope, you so far do not know. Do you think that is a quality response? – eyllanesc Aug 27 '18 at 22:47
  • @eyllanesc you are undoubtedly one of the best in PyQt, publish your decision, do not tire us with expectations. – S. Nick Aug 27 '18 at 23:19
  • 1
    @S.Nick I will say every time you see that a question can cause confusion, we respond that we have the responsibility that the answers we propose are the best and most clear we can do, so I ask you: your answer answers the OP question ?, that is, *How to make a Qtimer work on a Qthread?* – eyllanesc Aug 27 '18 at 23:28