Skip to content
Advertisement

Worker thread does not respond to slot calls from main thread

For my project based on Python and Qt I wanted to move expensive calculations and functions providing server/client functions into separate threads, to unfreeze my GUI. While leaving them running, I still want them to check periodically if there is new data from the main thread. For testing, I therefore implemented the following demo code:

import sys
from time import sleep
import shiboken6

from PySide6.QtCore import Qt, QObject, QThread, Signal, Slot, QTimer
from PySide6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Worker(QObject):
    finished = Signal()
    progress = Signal(int)

    def __init__(self):
        super().__init__()
        self.print_to_console_plz = False

    @Slot()
    def print_on_console_while_running(self):
        self.print_to_console_plz = True
        print("Set print_to_console to true")

    def run(self):
        timer = QTimer()
        for i in range(5):
            sleep(0.9)
            timer.start(100)
            if self.print_to_console_plz:
                print("Hello World from worker")
                self.print_to_console_plz = False
            self.progress.emit(i + 1)
        self.finished.emit()

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

    def initWorker(self):
        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.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)

    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.clicksToConsoleLabel = QLabel("Click here to print to console", self)
        self.clicksToConsoleLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.ToConsoleBttn = QPushButton("Print to console!", self)
        self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running)
        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.clicksToConsoleLabel)
        layout.addWidget(self.ToConsoleBttn)
        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):
        """Long-running task in 5 steps."""
        # Step 6: Start the thread
        if not shiboken6.isValid(self.thread):
            self.initWorker()
            self.ToConsoleBttn.clicked.connect(self.worker.print_on_console_while_running)
        self.thread.start()

        # Final resets
        self.longRunningBtn.setEnabled(False)
        self.thread.finished.connect(
            lambda: self.longRunningBtn.setEnabled(True)
        )
        self.thread.finished.connect(
            lambda: self.stepLabel.setText("Long-Running Step: 0")
        )

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

My main aim was to let the “expensive” function run in the worker thread while counting up, but it should still check periodically if there is new data available (represented by a call to print_on_console_while_running). To avoid having the run-function blocking everything while it is executed, I also introduced a QTimer as non-blocking timer.

Still, regardless, whenever I press on the button with “Print to console!” while the worker is executing the run-function, I always get “Set print_to_console to true” printed after the run-function has finished, and not during execution, indicating that the run-function still blocks the execution of everything else.

What am I doing wrong here, and how can I send data from the main thread to the worker thread while still executing the run-function?

Advertisement

Answer

The problem is caused by the fact that the slot is in the receiver thread, so Qt automatically uses a QueuedConnection:

The slot is invoked when control returns to the event loop of the receiver’s thread. The slot is executed in the receiver’s thread.

Since the thread is occupied with the execution of run(), print_on_console_while_running will be called only as soon as run() returns.

A possible solution is to force a direct connection:

The slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread.

    self.ToConsoleBttn.clicked.connect(
        self.worker.print_on_console_while_running, Qt.DirectConnection)

In this way, the slot is immediately called and the variable is instantly set.

Another, common approach (as long as the thread doesn’t need an actual event loop) is to directly subclass QThread and just override its run().

Since the QThread is the handler of the thread (no moveToThread is required), any connection made to any of its functions/slots will be in the same thread in which it was created (so, normally, the main thread), and only the run() will be executed in the separate thread, which means that implementing a print_on_console_while_running in that QThread subclass will always use a direct connection automatically.

Note that if you intend to start again the thread after its finished, you shouldn’t need to delete and recreate it again. Also note that that QTimer you’re creating is completely useless, not only because it doesn’t do anything when it times out, but mostly because time.sleep would prevent its processing. Finally, it’s usually better to avoid lambdas for thread connections, especially if the object is going to be destroyed.

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement