[Python] How can I let a Worker thread receive signals?

Discussão em 'Python' iniciado por Stack, Abril 13, 2025.

  1. Stack

    Stack Membro Participativo

    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...

Compartilhe esta Página