使用 concurrent.futures.ThreadPoolExecutor() 时的 PyQt5 小部件 Qthread 问题

Posted

技术标签:

【中文标题】使用 concurrent.futures.ThreadPoolExecutor() 时的 PyQt5 小部件 Qthread 问题【英文标题】:PyQt5 widget Qthread issue when using concurrent.futures.ThreadPoolExecutor() 【发布时间】:2020-10-23 13:23:45 【问题描述】:

我一直在尝试使用concurrent.futures.ThreadPoolExecutor() 在我的应用程序中运行一些后台任务,以便在这些任务(“测量”)运行时能够与 GUI 交互。完成这些任务后,我分配一个回调函数来更新 GUI 的某些字段,然后尝试根据这些字段更新 GUI 小部件(绘图、表格、列表等)。

这是一个例子:

class MainWindow(QtWidgets.QMainWindow):
    
    def __init__(self):
        super(MainWindow, self).__init__()
        *some more code goes here*
        self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)

    def perform_measurement():
        future = self.executor.submit(*a function*)
        future.add_done_callback(self.update_gui_fields)

    def update_gui_fields(self, future):
        data = future.result()
        self.items_for_list.append(QStandardItem(data['key']))
        *more fields are updated here*

        self.QListView1.setModel(self.items_for_list)
        *more widgets are updated here*

问题是字段更新正常,但是当我尝试与小部件交互时,应用程序崩溃了。这是因为子代(此处为self.items_for_list)与父代(此处为self.QListView1)处于不同的线程中。这是我得到的错误:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QListView(0x555795efbc10), parent's thread is QThread(0x555795296600), current thread is QThread(0x7fd12400a100)
QBasicTimer::start: QBasicTimer can only be used with threads started with QThread

我在以前的帖子中找不到任何解决方案。知道如何攻击这个吗? 谢谢!

【问题讨论】:

您应该使用 Qt 线程而不是 Python 线程。看看there 它的一般工作原理和/或搜索所以有很多类似的问题...... 【参考方案1】:

与 add_done_callback 关联的回调在辅助线程中执行,根据您的代码,您尝试从该辅助线程更新 GUI,这是被禁止的,因此 Qt 会抛出该警告。解决方案是通过创建一个通过信号转发该信息的 QObject 来实现逻辑:

import concurrent.futures
import sys
import time

from PyQt5 import QtCore, QtGui, QtWidgets


def measure():
    time.sleep(5)
    return "key": "value"


class TaskManager(QtCore.QObject):
    finished = QtCore.pyqtSignal(object)

    def __init__(self, parent=None, max_workers=None):
        super().__init__(parent)
        self._executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)

    @property
    def executor(self):
        return self._executor

    def submit(self, fn, *args, **kwargs):
        future = self.executor.submit(fn, *args, **kwargs)
        future.add_done_callback(self._internal_done_callback)

    def _internal_done_callback(self, future):
        data = future.result()
        self.finished.emit(data)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.model = QtGui.QStandardItemModel()
        self.view = QtWidgets.QListView()
        self.view.setModel(self.model)

        self.button = QtWidgets.QPushButton("launch")

        self._manager = TaskManager(max_workers=1)
        self._manager.finished.connect(self.update_gui_fields)

        self.button.clicked.connect(self.perform_measurement)

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        lay = QtWidgets.QVBoxLayout(central_widget)
        lay.addWidget(self.view)
        lay.addWidget(self.button)

    def perform_measurement(self):
        self._manager.submit(measure)

    def update_gui_fields(self, data):
        self.model.appendRow(QtGui.QStandardItem(data["key"]))


if __name__ == "__main__":

    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

【讨论】:

以上是关于使用 concurrent.futures.ThreadPoolExecutor() 时的 PyQt5 小部件 Qthread 问题的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)