Why does pyqt subthread freezes GUI when doing heavy repetitive work?

35 views Asked by At

In this simulated example I am repeating a task every 1 second using QTimer by implementing a QObject, then move it to a QThead. The simulated task takes 0.5 second. The task runs in a new thread, which should not have interfered with the main thread in which the GUI is running. However, the task will freeze the GUI repetitively.

import sys
from time import sleep

from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QTimer
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Worker(QObject):
    progress = pyqtSignal(int)
    
    def __init__(self):
        super().__init__()
        self.i=0
        self.Timer=QTimer(self)
        self.Timer.timeout.connect(self.run)
        self.Timer.setInterval(1000)

    def run(self):
        """Long-running task."""
        sleep(0.5)
        self.i=self.i+1
        self.progress.emit(self.i)


class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.clicksCount = 0
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Freezing GUI")
        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        # Create and connect widgets
        self.clicksLabel = QLabel("Counting: 0 clicks", self)
        self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.stepLabel = QLabel("Long-Running Step: 0")
        self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.longRunningBtn = QPushButton("Long-Running Task!", self)
        self.longRunningBtn.clicked.connect(self.runLongTask)
        # Set the layout
        layout = QVBoxLayout()
        layout.addWidget(self.clicksLabel)
        layout.addWidget(self.countBtn)
        layout.addStretch()
        layout.addWidget(self.stepLabel)
        layout.addWidget(self.longRunningBtn)
        self.centralWidget.setLayout(layout)

    def countClicks(self):
        self.clicksCount += 1
        self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")

    def reportProgress(self, n):
        self.stepLabel.setText(f"Long-Running Step: {n}")
        
        
    def runLongTask(self):
        # Step 2: Create a QThread object
        self.thread = QThread()
        # Step 3: Create a worker object
        self.worker = Worker()
        # Step 4: Move worker to the thread
        self.worker.moveToThread(self.thread)
        # Step 5: Connect signals and slots
        
        self.thread.started.connect(self.worker.Timer.start)

        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        # Step 6: Start the thread
        self.thread.start()


        self.longRunningBtn.setEnabled(False)


app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())

If I reduce the work in the task by removing sleep() or setting a smaller time for the sleep() function in Worker.run(), then the GUI will not freeze. However, realistically I am doing a task that requires a long time to complete, which makes it a problem.

1

There are 1 answers

3
WEINAN WANG On

Thanks for the ideas and inputs from the comments. It turns out that the problem comes from the fact that run is a member function of the Worker class, but is not a Signal or Slot of the Worker class. Moving the worker to the new thread only affect its Signals and Slots, not run. So run is still executed in the main thread.

One solution will be to use the @pyqtSlot() decorator when defining run, to make it a Slot of the Worker class. Also, specifying DirectionConnection between Timer.timeout and Worker.run can also force the run function to run in the newly created thread that Timer has been moved to.