使用 QRunnable 进行线程处理 - 发送双向回调的正确方式

Posted

技术标签:

【中文标题】使用 QRunnable 进行线程处理 - 发送双向回调的正确方式【英文标题】:Threading with QRunnable - Proper manner of sending bi-directional callbacks 【发布时间】:2020-08-20 19:35:27 【问题描述】:

From a tutorial,我看到信号和槽可用于从工作线程回调到主 GUI 线程,但我不确定如何使用信号和槽建立双向通信。以下是我正在使用的:

class RespondedToWorkerSignals(QObject):
    callback_from_worker = pyqtSignal()

class RespondedToWorker(QRunnable):
    def __init__(self, func, *args, **kwargs):
        super(RespondedToWorker, self).__init__()
        self._func = func
        self.args = args
        self.kwargs = kwargs
        self.signals = RespondedToWorkerSignals()
        self.kwargs['signal'] = self.signals.callback_from_worker
        print("Created a responded-to worker")

    @pyqtSlot()
    def run(self):
        self._func(*self.args, **self.kwargs)

    @pyqtSlot()
    def acknowledge_callback_in_worker(self):
        print("Acknowledged Callback in Worker")

class MainWindow(QMainWindow):

    # Signal meant to connect to a slot present within a worker
    mainthread_callback_to_worker = pyqtSignal()

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        # Quick UI setup
        w, lay = QWidget(), QVBoxLayout()
        w.setLayout(lay)
        self.setCentralWidget(w)
        self.timer_label = QLabel("Timer Label")
        lay.addWidget(self.timer_label)
        self.btn_thread_example = QPushButton("Push Me")
        self.btn_thread_example.pressed.connect(self.thread_example)
        lay.addWidget(self.btn_thread_example)
        self.threadpool = QThreadPool()
        self.show()

        # Set up QTimer to continue in the background to help demonstrate threading advantage
        self.counter = 0
        self.timer = QTimer()
        self.timer.setInterval(1000)
        self.timer.timeout.connect(self.recurring_timer)
        self.timer.start()

    @pyqtSlot()
    def do_something(self, signal):
        # signal argument will be the callback_from_worker and it will emit to acknowledge_callback_in_mainthread
        print("do_something is sleeping briefly. Try to see if you get a locked widget...")
        time.sleep(7)
        signal.emit()

    @pyqtSlot()
    def acknowledge_callback_in_mainthread_and_respond(self):
        # this function should respond to callback_from_worker and emit a response
        print("Acknowledged Callback in Main")
        self.mainthread_callback_to_worker.emit()

    def thread_example(self):
        print("Beginning thread example")
        worker = RespondedToWorker(self.do_something)
        worker.signals.callback_from_worker.connect(self.acknowledge_callback_in_mainthread_and_respond)
    # self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash

    def recurring_timer(self):
        self.counter += 1
        self.timer_label.setText(f"Counter: self.counter")

if __name__ == '__main__':
    app = QApplication(sys.argv)
    win = MainWindow()
    app.setStyle("Fusion")
    win.show()
    sys.exit(app.exec())

目前,脚本可以创建第二个线程并向主 GUI 发送信号。我希望 GUI 将响应信号发送回工作线程。我也不确定为什么连接主/GUI的信号mainthread_callback_to_worker会导致崩溃(请参阅注释掉的行)。

我了解一种解决方法是让do_something 返回一些值,然后在工作人员内部将其用作“确认”。但如果可能的话,我想知道使用信号和插槽的解决方案。

【问题讨论】:

您似乎没有阅读该站点的文档,因此建议您检查How to Ask并通过tour。此处未通过添加 SOLVED 修改标题或编辑问题以添加可能的解决方案,此处您必须将答案标记为正确并在答案部分发布答案。注意:您应该始终阅读您使用的技术的文档(例如 Qt 和 *** 文档) 【参考方案1】:

要了解错误原因,您必须在终端中运行代码,您将收到以下错误消息:

QObject::connect: Cannot connect MainWindow::mainthread_callback_to_worker() to (nullptr)::acknowledge_callback_in_worker()
Traceback (most recent call last):
  File "main.py", line 72, in thread_example
    self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
TypeError: connect() failed between MainWindow.mainthread_callback_to_worker[] and acknowledge_callback_in_worker()
Aborted (core dumped)

错误的原因是pyqtSlot 装饰器的滥用,因为它应该只在QObject 方法中使用,但 QRunnable 不会导致该异常,此外在非 QObject 中它不需要任何优势,使 run() 方法中的装饰器没有意义。

另一方面,QRunnable 只是一个存在于主线程中的接口,并且只有 run 方法在另一个线程中执行,因此 QRunnable 不能成为工作线程,因为该类型的目标必须在辅助线程中执行其方法线程。

因此,使用上述 QRunnable 不是合适的选项,因此出于您的目的,我建议使用位于辅助线程中并调用方法的 QObject。

import sys
import time

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer, QThread
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QWidget,
    QVBoxLayout,
    QLabel,
    QPushButton,
)


class Worker(QObject):
    callback_from_worker = pyqtSignal()

    def __init__(self, func, *args, **kwargs):
        super(Worker, self).__init__()
        self._func = func
        self.args = args
        self.kwargs = kwargs
        self.kwargs["signal"] = self.callback_from_worker

    def start_task(self):
        QTimer.singleShot(0, self.task)

    @pyqtSlot()
    def task(self):
        self._func(*self.args, **self.kwargs)

    @pyqtSlot()
    def acknowledge_callback_in_worker(self):
        print("Acknowledged Callback in Worker")
        print(threading.current_thread())


class MainWindow(QMainWindow):
    mainthread_callback_to_worker = pyqtSignal()

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        w, lay = QWidget(), QVBoxLayout()
        w.setLayout(lay)
        self.setCentralWidget(w)
        self.timer_label = QLabel("Timer Label")
        lay.addWidget(self.timer_label)
        self.btn_thread_example = QPushButton("Push Me")
        self.btn_thread_example.pressed.connect(self.thread_example)
        lay.addWidget(self.btn_thread_example)

        self.counter = 0
        self.timer = QTimer(interval=1000, timeout=self.recurring_timer)
        self.timer.start()

        self._worker = Worker(self.do_something)
        self._worker.callback_from_worker.connect(
            self.acknowledge_callback_in_mainthread_and_respond
        )

        self.worker_thread = QThread(self)
        self.worker_thread.start()
        self._worker.moveToThread(self.worker_thread)

    @pyqtSlot()
    def do_something(self, signal):
        print(
            "do_something is sleeping briefly. Try to see if you get a locked widget..."
        )
        time.sleep(7)
        signal.emit()

    @pyqtSlot()
    def acknowledge_callback_in_mainthread_and_respond(self):
        print("Acknowledged Callback in Main")
        self.mainthread_callback_to_worker.emit()

    def thread_example(self):
        print("Beginning thread example")
        self._worker.start_task()

    def recurring_timer(self):
        self.counter += 1
        self.timer_label.setText(f"Counter: self.counter")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    win = MainWindow()
    app.setStyle("Fusion")
    win.show()
    ret = app.exec_()
    win.worker_thread.quit()
    win.worker_thread.wait()
    sys.exit(ret)

【讨论】:

感谢您的解释。经过进一步研究,我还想让任何读者知道,根据to the docs,我犯的另一个错误是缺少 QRunnable+QThreadPool 不支持能够“通过信号更新数据”的细节"并且它们也不支持被“使用信号控制”。

以上是关于使用 QRunnable 进行线程处理 - 发送双向回调的正确方式的主要内容,如果未能解决你的问题,请参考以下文章

C++/Qt - QThread 与 QRunnable

QObject::connect 在 QRunnable - 控制台

重用 QRunnable

完成后将 QRunnable 连接到函数/方法

如何杀死 PyQt5 中的 QRunnable?

无法写入 QRunnable 内的 QTcpSocket