Pyqtgraph Внутренний объект C ++ (ChildGroup) уже удален в GUI - PullRequest
0 голосов
/ 16 марта 2020

Я делаю GUI, чтобы собрать данные с устройства и построить это. Моя цель - отделить основной поток от потоков построения и сбора данных. Я делаю это с помощью двух дополнительных рабочих, poc_worker и plotting_worker.

При запуске моего GUI все идет хорошо до определенного момента, когда один из моих графиков внезапно становится пустым, и pyqtgraph бросает меня:

  Traceback (most recent call last):
  File "c:\git_repositories\project\build\python-venv-win\lib\site-packages\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 75, in boundingRect
    return self.mapRectFromParent(self.parentItem().boundingRect())
RuntimeError: Internal C++ object (ChildGroup) already deleted.

Я прочитал об этой ошибке и обнаружил ее может быть связано с неправильным хранением ваших объектов и python сборкой мусора. Однако я храню все свои объекты. Это код:

Рабочий po c определяется как:

class PocWorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """

    finished = Signal()
    isRunning = Signal(bool)
    data = Signal(object)


class PocWorker(QRunnable):
    """
    PocWorker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    Intended to run continuously on an the given object by calling obj.update with the args and kwargs

    Can by stopped by calling stop()

    """

    def __init__(self, obj, *args, **kwargs):
        super(PocWorker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.obj = obj
        self.args = args
        self.kwargs = kwargs
        self._isRunning = False
        self.signals = PocWorkerSignals()

    @Slot()  # QtCore.Slot
    def run(self):
        """
        Initialise the runner function with passed args, kwargs.
        """
        self._isRunning = True
        self.signals.isRunning.emit(True)
        while self._isRunning:
            data = self.obj.update(*self.args, **self.kwargs)
            self.signals.data.emit(data)
        self._stop()

    @Slot()
    def stop(self):
        self._isRunning = False
        self._stop()

    def _stop(self):
        self.signals.isRunning.emit(False)
        self.signals.finished.emit()

    def isRunning(self):
        return self._isRunning


class DataGenerator:
    def __init__(self):
        self.data_size = (6, 2, 4000)
        self.update_counter = 0

    def update(self):
        time.sleep(0.05)

        # Generate data:
        plot_data = np.random.rand(*self.data_size)
        data = {"plot_data": plot_data, "update_counter": self.update_counter}

        # Increase update counter:
        self.update_counter += 1

        return data

Этот рабочий генерирует случайные данные со временем обновления, близким к 0,05 секундам. Рабочий-чертёж стремится строить графики через регулярные промежутки времени и определяется следующим образом:

class PlottingWorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """

    finished = Signal()
    isRunning = Signal(bool)
    update_non_threadable = Signal(object)


class PlottingWorker(QObject):
    """
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    """

    def __init__(self, obj, polling_interval=0.25):

        super(PlottingWorker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.signals = PlottingWorkerSignals()

        self._last_update_time = datetime.now()
        self._polling_interval = polling_interval
        self._update_scheduled = False

        self.obj = obj

    @Slot(object)
    def update(self, data):
        self._data = data
        self._schedule_update()

    def _schedule_update(self):
        if not self._update_scheduled:
            self._update_scheduled = True
            elapsed = (datetime.now() - self._last_update_time).total_seconds()
            delay = self._polling_interval - elapsed
            QTimer.singleShot(max(0, delay * 1000), self._do_update)

    @Slot()
    def _do_update(self):
        self._update_scheduled = False
        self._last_update_time = datetime.now()
        self.obj.update(self._data)
        self.signals.update_non_threadable.emit(self._data)


class Plotter:
    def __init__(self, graphs, plots):
        self.graphs = graphs
        self.plots = plots

    def update(self, data):
        # print("Updating plots")
        for m, plot in enumerate(self.plots):
            # print(f"Updating plot {m}")
            for l, line in enumerate(plot):
                # print(f"Updating line {l}")
                line.setData(data["plot_data"][m, l, :])

    def update_non_threadable(self, data):
        # print("Updating titles")
        title = f"hello {data['update_counter']}"
        for graph in self.graphs:
            graph.setTitle(title)

Класс плоттера делится на два метода: один для обновления данных и один для обновления заголовка. Обновление заголовка из другого потока было не для go для pyqt, поэтому я отделил это. Это связано вместе:

class Main(QWidget):
    def __init__(self):
        super().__init__()

        # Create widgets
        self.create_widgets()

        # POC setup:
        self.poc = DataGenerator()
        self.poc_worker = PocWorker(self.poc)

        # Plotting setup:
        self.plotter = Plotter(plots=self.plots, graphs=self.graphs)
        self.plot_thread = QThread()
        self.plot_worker = PlottingWorker(self.plotter)
        self.plot_worker.moveToThread(self.plot_thread)

        # UI connections:
        self.setup_ui_connections()

        # Worker connections:
        self.setup_worker_connections()

        # Setup thread-pool
        self.threadpool = QThreadPool()

    def create_widgets(self):
        self.layout = QGridLayout(self)

        self.n_graphs = 6
        self.n_vert = 3
        self.n_hor = 2
        self.n_lines = 2

        self.graphs = [None for i in range(self.n_graphs)]
        self.plots = [[None for i in range(self.n_lines)] for j in range(self.n_graphs)]

        graph_no = 0
        colors = ["r", "g"]
        for m in range(self.n_vert):
            for n in range(self.n_hor):
                self.graphs[graph_no] = pg.PlotWidget()
                self.layout.addWidget(self.graphs[graph_no], m, n)

                for l in range(self.n_lines):
                    self.plots[graph_no][l] = self.graphs[graph_no].plot(pen=colors[l])

                graph_no += 1

        self.status_label = QLabel("Start")
        self.start_button = QPushButton("Start data gathering")
        self.stop_button = QPushButton("Stop data gathering")

        self.layout.addWidget(self.start_button)
        self.layout.addWidget(self.stop_button)
        self.layout.addWidget(self.status_label)

        self.show()

    def setup_ui_connections(self):
        self.start_button.pressed.connect(self.start_plotter)
        self.start_button.pressed.connect(self.start_poc)
        self.stop_button.pressed.connect(self.stop_poc)

    def setup_worker_connections(self):
        self.poc_worker.signals.data.connect(self.plot_worker.update)
        self.plot_worker.signals.update_non_threadable.connect(self.plotter.update_non_threadable)

    @Slot(str)
    def update_label(self, label_text):
        # print("Updating label")
        self.status_label.setText(label_text)

    @Slot(object)
    def update_titles(self, titles):
        # print("Updating titles")
        for graph, title in zip(self.graphs, titles):
            graph.setTitle(title)

    @Slot()
    def start_plotter(self):
        print("Starting plotter worker")
        self.plot_thread.start()

    @Slot()
    def stop_plotter(self):
        print("Stopping plotter worker")
        self.plot_thread.exit()

    @Slot()
    def start_poc(self):
        print("Starting poc worker")
        self.threadpool.start(self.poc_worker)

    @Slot()
    def stop_poc(self):
        print("Stopping poc worker")
        self.poc_worker.stop()

    def closeEvent(self, event):
        print("Closing application")
        self.stop_plotter()
        self.stop_poc()
        self.stop_data_buffer()

        time.sleep(0.5)


if __name__ == "__main__":
    app = QApplication(sys.argv)

    widget = Main()

    sys.exit(app.exec_())

Как вы можете видеть при запуске, self.plots инициализируются пустыми данными из основного потока. После этого рабочий-чертёж создается и перемещается в собственный поток. Рабочий на графике запускается рабочим po c, и по прошествии достаточного времени выполняется обновление графика. Всякий раз, когда я запускаю это, через некоторое время я получаю

  Traceback (most recent call last):
  File "c:\git_repositories\project\build\python-venv-win\lib\site-packages\pyqtgraph\graphicsItems\ViewBox\ViewBox.py", line 75, in boundingRect
    return self.mapRectFromParent(self.parentItem().boundingRect())
RuntimeError: Internal C++ object (ChildGroup) already deleted.

, и один из графиков становится пустым, и мое приложение вылетает. Что я здесь не так делаю? Это похоже на состояние гонки, но я не могу понять, как?

Для полноты, это мой код с импортом:

import sys
import time

from datetime import datetime

import numpy as np

import pyqtgraph as pg

from PySide2.QtWidgets import QWidget, QApplication, QGridLayout, QLabel, QPushButton
from PySide2.QtCore import QThreadPool, QObject, QRunnable, Slot, QTimer, Signal, QThread, QMutex


class PocWorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """

    finished = Signal()
    isRunning = Signal(bool)
    data = Signal(object)


class PocWorker(QRunnable):
    """
    PocWorker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    Intended to run continuously on an the given object by calling obj.update with the args and kwargs

    Can by stopped by calling stop()

    """

    def __init__(self, obj, *args, **kwargs):
        super(PocWorker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.obj = obj
        self.args = args
        self.kwargs = kwargs
        self._isRunning = False
        self.signals = PocWorkerSignals()

    @Slot()  # QtCore.Slot
    def run(self):
        """
        Initialise the runner function with passed args, kwargs.
        """
        self._isRunning = True
        self.signals.isRunning.emit(True)
        while self._isRunning:
            data = self.obj.update(*self.args, **self.kwargs)
            self.signals.data.emit(data)
        self._stop()

    @Slot()
    def stop(self):
        self._isRunning = False
        self._stop()

    def _stop(self):
        self.signals.isRunning.emit(False)
        self.signals.finished.emit()

    def isRunning(self):
        return self._isRunning


class DataGenerator:
    def __init__(self):
        self.data_size = (6, 2, 4000)
        self.update_counter = 0

    def update(self):
        time.sleep(0.05)

        # Generate data:
        plot_data = np.random.rand(*self.data_size)
        data = {"plot_data": plot_data, "update_counter": self.update_counter}

        # Increase update counter:
        self.update_counter += 1

        return data


class PlottingWorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.
    """

    finished = Signal()
    isRunning = Signal(bool)
    update_non_threadable = Signal(object)


class PlottingWorker(QObject):
    """
    Worker thread

    Inherits from QRunnable to handler worker thread setup, signals and wrap-up.

    :param callback: The function callback to run on this worker thread. Supplied args and
                     kwargs will be passed through to the runner.
    :type callback: function
    :param args: Arguments to pass to the callback function
    :param kwargs: Keywords to pass to the callback function

    """

    def __init__(self, obj, polling_interval=0.25):

        super(PlottingWorker, self).__init__()
        # Store constructor arguments (re-used for processing)
        self.signals = PlottingWorkerSignals()

        self._last_update_time = datetime.now()
        self._polling_interval = polling_interval
        self._update_scheduled = False

        self.obj = obj

    @Slot(object)
    def update(self, data):
        self._data = data
        self._schedule_update()

    def _schedule_update(self):
        if not self._update_scheduled:
            self._update_scheduled = True
            elapsed = (datetime.now() - self._last_update_time).total_seconds()
            delay = self._polling_interval - elapsed
            QTimer.singleShot(max(0, delay * 1000), self._do_update)

    @Slot()
    def _do_update(self):
        self._update_scheduled = False
        self._last_update_time = datetime.now()
        self.obj.update(self._data)
        self.signals.update_non_threadable.emit(self._data)


class Plotter:
    def __init__(self, graphs, plots):
        self.graphs = graphs
        self.plots = plots

    def update(self, data):
        # print("Updating plots")
        for m, plot in enumerate(self.plots):
            # print(f"Updating plot {m}")
            for l, line in enumerate(plot):
                # print(f"Updating line {l}")
                line.setData(data["plot_data"][m, l, :])

    def update_non_threadable(self, data):
        # print("Updating titles")
        title = f"hello {data['update_counter']}"
        for graph in self.graphs:
            graph.setTitle(title)


class Main(QWidget):
    def __init__(self):
        super().__init__()

        # Create widgets
        self.create_widgets()

        # POC setup:
        self.poc = DataGenerator()
        self.poc_worker = PocWorker(self.poc)

        # Plotting setup:
        self.plotter = Plotter(plots=self.plots, graphs=self.graphs)
        self.plot_thread = QThread()
        self.plot_worker = PlottingWorker(self.plotter)
        self.plot_worker.moveToThread(self.plot_thread)

        # UI connections:
        self.setup_ui_connections()

        # Worker connections:
        self.setup_worker_connections()

        # Setup thread-pool
        self.threadpool = QThreadPool()

    def create_widgets(self):
        self.layout = QGridLayout(self)

        self.n_graphs = 6
        self.n_vert = 3
        self.n_hor = 2
        self.n_lines = 2

        self.graphs = [None for i in range(self.n_graphs)]
        self.plots = [[None for i in range(self.n_lines)] for j in range(self.n_graphs)]

        graph_no = 0
        colors = ["r", "g"]
        for m in range(self.n_vert):
            for n in range(self.n_hor):
                self.graphs[graph_no] = pg.PlotWidget()
                self.layout.addWidget(self.graphs[graph_no], m, n)

                for l in range(self.n_lines):
                    self.plots[graph_no][l] = self.graphs[graph_no].plot(pen=colors[l])

                graph_no += 1

        self.status_label = QLabel("Start")
        self.start_button = QPushButton("Start data gathering")
        self.stop_button = QPushButton("Stop data gathering")

        self.layout.addWidget(self.start_button)
        self.layout.addWidget(self.stop_button)
        self.layout.addWidget(self.status_label)

        self.show()

    def setup_ui_connections(self):
        self.start_button.pressed.connect(self.start_plotter)
        self.start_button.pressed.connect(self.start_poc)
        self.stop_button.pressed.connect(self.stop_poc)

    def setup_worker_connections(self):
        self.poc_worker.signals.data.connect(self.plot_worker.update)
        self.plot_worker.signals.update_non_threadable.connect(self.plotter.update_non_threadable)

    @Slot(str)
    def update_label(self, label_text):
        # print("Updating label")
        self.status_label.setText(label_text)

    @Slot(object)
    def update_titles(self, titles):
        # print("Updating titles")
        for graph, title in zip(self.graphs, titles):
            graph.setTitle(title)

    @Slot()
    def start_plotter(self):
        print("Starting plotter worker")
        self.plot_thread.start()

    @Slot()
    def stop_plotter(self):
        print("Stopping plotter worker")
        self.plot_thread.exit()

    @Slot()
    def start_poc(self):
        print("Starting poc worker")
        self.threadpool.start(self.poc_worker)

    @Slot()
    def stop_poc(self):
        print("Stopping poc worker")
        self.poc_worker.stop()

    def closeEvent(self, event):
        print("Closing application")
        self.stop_plotter()
        self.stop_poc()
        self.stop_data_buffer()

        time.sleep(0.5)


if __name__ == "__main__":
    app = QApplication(sys.argv)

    widget = Main()

    sys.exit(app.exec_())

Вот пакеты, которые я использую:

appdirs==1.4.3
astroid==2.3.3
attrs==19.3.0
black==19.10b0
Click==7.0
colorama==0.4.3
isort==4.3.21
lazy-object-proxy==1.4.3
mccabe==0.6.1
numpy==1.18.1
pathspec==0.7.0
pylint==2.4.4
pyqtgraph==0.11.0rc0
PySide2==5.14.1
regex==2020.2.20
scipy==1.4.1
shiboken2==5.14.1
six==1.14.0
toml==0.10.0
typed-ast==1.4.1
wrapt==1.11.2

Я использую эту конкретную c версию pyqtgraph, потому что он может работать с PySide2. Скриншот, когда он идет не так:

enter image description here

...