PyQt5 widget Qthread issue when using concurrent.futures.ThreadPoolExecutor()

Viewed 527

I have been trying to use the concurrent.futures.ThreadPoolExecutor() to run some background tasks in my application, so that I will be able to interact with the GUI while these tasks ("measurements") run. Once these tasks are finished I assign a callback function that updates some fields of the GUI then tries to update the GUI widgets (plots, tables, lists etc.) based on these fields.

Here is an example:

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*

The problem is that the fields are updated normally, but when I try to interact with the widgets the app crashes. This is because the children (here the self.items_for_list) are in a different thread than the parent (here self.QListView1). This is the error that I get:

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

I couldn't find any solution on previous posts. Any idea of how to attack this? Thanks!

1 Answers

The callback associated with add_done_callback is executed in a secondary thread, and according to your code you are trying to update the GUI from that secondary thread, which is forbidden, so Qt throws that warning. The solution is to implement the logic by creating a QObject that forwards that information through signals:

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