简体   繁体   English

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

[英]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.对于基于 Python 和 Qt 的项目,我想将提供服务器/客户端功能的昂贵计算和功能移动到单独的线程中,以解冻我的 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 ).我的主要目标是让“昂贵”的 function 在工作线程中运行,同时计数,但它仍应定期检查是否有新数据可用(通过调用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.为了避免 run-function 在执行时阻塞所有内容,我还引入了QTimer作为非阻塞计时器。

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.尽管如此,无论如何,每当我在工作人员执行运行功能时按下“打印到控制台”按钮时,我总是在run功能完成后打印“将 print_to_console 设置为 true”,而不是在执行期间打印,表明run-function 仍然会阻止其他所有内容的执行。

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

The problem is caused by the fact that the slot is in the receiver thread, so Qt automatically uses a QueuedConnection :问题是由于插槽位于接收线程中,因此 Qt 自动使用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.由于线程被run()的执行占用, print_on_console_while_running将仅在run()返回时被调用。

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() .另一种常见的方法(只要线程不需要实际的事件循环)是直接继承 QThread 并覆盖它的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.由于 QThread 是线程的处理程序(不需要moveToThread ),因此与其任何函数/插槽建立的任何连接都将在创建它的同一线程中(因此,通常是主线程),并且只有run()将在单独的线程中执行,这意味着在该 QThread 子类中实现print_on_console_while_running将始终自动使用直接连接。

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.另请注意,您正在创建的 QTimer 完全没用,不仅因为它在超时时不做任何事情,而且主要是因为time.sleep会阻止它的处理。 Finally, it's usually better to avoid lambdas for thread connections, especially if the object is going to be destroyed.最后,通常最好避免使用 lambda 进行线程连接,尤其是在 object 将被破坏的情况下。

Another way of looking at this, is that the problem is caused by your worker running a blocking loop, which stops all event processing within its own thread .另一种看待这个问题的方式是,问题是由您的工作人员运行一个阻塞循环引起的,该循环停止了它自己的线程中的所有事件处理。 This will affect incoming queued signals and timers started within the worker thread, which both need to post events to the worker thread's event-queue.这将影响在工作线程中启动的传入排队信号和计时器,它们都需要将事件发布到工作线程的事件队列。 So you've effectively moved a small part of the freezing behaviour from the gui-thread to the worker-thread因此,您已经有效地将一小部分冻结行为从 gui 线程移到了工作线程

(Note that if you comment out the signal connection that deletes the worker, any "Set print_to_console to true" messages will be belatedly printed, since the worker can no longer block its own thread after its finished). (请注意,如果您注释掉删除工作人员的信号连接,任何“将 print_to_console 设置为 true”的消息都将延迟打印,因为工作人员在完成后不能再阻塞自己的线程)。

If you want to continue using queued signals and thread-local timers, you can periodically enforce processing of thread-local events from within your blocking loop.如果您想继续使用排队信号和线程本地计时器,您可以从阻塞循环中定期强制处理线程本地事件。 For timers to work (somewhat) precisely, this obviously means you must do this more frequently than the timers are scheduled to timeout.为了让计时器(有点)精确地工作,这显然意味着您必须比计时器安排的超时更频繁地执行此操作。 So something like this should work as expected:所以这样的事情应该按预期工作:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM