工作线程不响应来自主线程的槽调用

Worker thread does not respond to slot calls from main thread

提问人:arc_lupus 提问时间:7/25/2022 最后编辑:ekhumoroarc_lupus 更新时间:7/26/2022 访问量:330

问:

对于我基于 Python 和 Qt 的项目,我想将昂贵的计算和提供服务器/客户端功能的函数移动到单独的线程中,以解冻我的 GUI。在让它们保持运行的同时,我仍然希望它们定期检查主线程是否有新数据。因此,为了进行测试,我实现了以下演示代码:

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())

我的主要目的是让“昂贵”的函数在计数时在工作线程中运行,但它仍然应该定期检查是否有新数据可用(由调用 )。为了避免运行函数在执行时阻塞所有内容,我还引入了一个非阻塞计时器。print_on_console_while_runningQTimer

尽管如此,无论如何,每当我在工作线程执行运行函数时按下“打印到控制台!”按钮时,我总是在 -function 完成后而不是在执行期间打印“Set print_to_console to true”,这表明运行函数仍然阻止执行其他所有内容。run

我在这里做错了什么,如何在仍然执行运行函数的同时将数据从主线程发送到工作线程?

python 多线程 事件 signals-slots pyside6

评论


答:

1赞 musicamante 7/25/2022 #1

该问题是由插槽位于接收方线程中这一事实引起的,因此Qt自动使用QueuedConnection

当控制返回到接收方线程的事件循环时,将调用该槽。插槽在接收方的线程中执行。

由于线程被 的执行所占用,因此只有在返回时才会调用。run()print_on_console_while_runningrun()

一个可能的解决方案是强制直接连接:

发出信号时,会立即调用时隙。插槽在信令线程中执行。

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

这样,插槽立即被调用,变量立即被设置。

另一种常见的方法(只要线程不需要实际的事件循环)是直接子类化 QThread 并覆盖其 .run()

由于 QThread 是线程的处理程序(不需要 no),因此与其任何函数/插槽建立的任何连接都将在创建它的同一线程中(因此,通常为主线程),并且只有 将在单独的线程中执行,这意味着在该 QThread 子类中实现 将始终自动使用直接连接。moveToThreadrun()print_on_console_while_running

请注意,如果您打算在线程完成后重新启动线程,则无需删除并重新创建它。还要注意的是,你正在创建的 QTimer 是完全无用的,不仅因为它在超时时不做任何事情,而且主要是因为 time.sleep 会阻止它的处理。最后,通常最好避免使用线程连接的 lambda,尤其是在对象将被销毁时。

2赞 ekhumoro 7/26/2022 #2

另一种看待这个问题的方式是,问题是由运行阻塞循环的工作线程引起的,该循环会停止其自身线程中的所有事件处理。这将影响在工作线程中启动的传入排队信号和计时器,它们都需要将事件发布到工作线程的事件队列。因此,您已经有效地将一小部分冻结行为从 gui-thread 转移到了 worker-thread!

(请注意,如果注释掉删除工作线程的信号连接,则任何“将print_to_console设置为 true”消息都将延迟打印,因为工作线程在完成后无法再阻止自己的线程)。

如果要继续使用排队信号和线程本地计时器,则可以在阻塞循环中定期强制处理线程本地事件。为了使计时器(在某种程度上)精确地工作,这显然意味着您必须比计时器计划超时更频繁地执行此操作。因此,像这样的东西应该按预期工作:

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")

    @Slot()
    def timeout_handler(self):
        print('timeout:', QTime.currentTime().toString('HH:mm:ss.z'))

    def run(self):
        timer = QTimer()
        timer.start(100)
        timer.timeout.connect(self.timeout_handler)
        for i in range(50):
            sleep(0.05)
            QApplication.processEvents()
            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()