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.