I'm building a PyQt6 application that can run a sequence in the background. The sequence is a subclass of QObject with a run() method that gets called by a QThread. Here's the problem: the sequence cannot receive signals. It has signals for pausing, unpausing, and cancelling. When I start the sequence, I link these signals to buttons in the main thread. However, pressing these buttons does not do anything. The button signals are properly emitted, but the Worker thread never receives them. I understand this is because the run() method of the Worker blocks the QThread's event loop, and the signals are QueuedConnections, so no event loop means no signals being processed. In all my searching, I've found two solutions: (1) use a QTimer to run events in the Worker instead of a while loop so the event loop isn't blocked, or (2) call QCoreApplication.processEvents() to process events manually. The first option is not my favorite because it requires me to restructure some of my code (oh no!), but I could do it if it's the only way. I've heard the second option is bad practice because it might not be thread safe (I'm still not sure about this), but I could do it because I'm frequently waiting in between sequence actions anyway. Is there any way to have the Worker receive signals other than the above options? For example, could I process just events in the Worker thread, not the whole application? Here's an example that models the problem (mostly ripped from here). import sys import time from PyQt6.QtCore import Qt, pyqtSignal, QObject, QThread, QReadLocker, QWriteLocker, QReadWriteLock from PyQt6.QtWidgets import ( QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, ) class Worker(QObject): finished = pyqtSignal() progress = pyqtSignal(int) cancelCommand = pyqtSignal() def __init__(self): super().__init__() self.canceled = False self.paused = False self.mutex = QReadWriteLock() def run(self): """Long-running task.""" for i in range(10): # this gets properly processed. Why? self.progress.emit(i) time.sleep(1) with QReadLocker(self.mutex): if self.canceled: break while True: with QReadLocker(self.mutex): if not self.paused: break time.sleep(0.1) self.finished.emit() def cancel(self): # this never runs with QWriteLocker(self.mutex): self.canceled = True def pause(self): # this never runs with QWriteLocker(self.mutex): self.paused = True def unpause(self): # this never runs with QWriteLocker(self.mutex): self.paused = False 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.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) self.stepLabel = QLabel("Long-Running Step: 0") self.stepLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.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) self.cancel_button = QPushButton("Cancel", self) self.pause_button = QPushButton("Pause", self) self.unpause_button = QPushButton("Unpause", self) # Set the layout layout = QVBoxLayout() layout.addWidget(self.clicksLabel) layout.addWidget(self.countBtn) layout.addStretch() layout.addWidget(self.stepLabel) layout.addWidget(self.longRunningBtn) layout.addWidget(self.cancel_button) layout.addWidget(self.pause_button) layout.addWidget(self.unpause_button) 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.threader = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.threader) # Step 5: Connect signals and slots self.threader.started.connect(self.worker.run) self.worker.finished.connect(self.threader.quit) self.worker.finished.connect(self.worker.deleteLater) self.threader.finished.connect(self.threader.deleteLater) self.worker.progress.connect(self.reportProgress) # these three signals never get received self.cancel_button.pressed.connect(self.worker.cancel) self.pause_button.pressed.connect(self.worker.pause) self.unpause_button.pressed.connect(self.worker.unpause) # Step 6: Start the thread self.threader.start() # Final resets self.longRunningBtn.setEnabled(False) self.threader.finished.connect(lambda: self.stepLabel.setText("Long-Running Step: 0")) app = QApplication(sys.argv) win = Window() win.show() sys.exit(app.exec()) Continue reading...