如何在运行多线程计算时防止崩溃,然后绘制发出的结果

问题描述 投票:1回答:1

我正在编写一个PySide2应用程序,它将结果绘制成某个计算并尝试多线程计算以避免锁定GUI。我正在尝试使用QThreadPool,在与绘图相关的选项进行交互时,在一个单独的线程中运行计算,该线程通过信号将结果返回到回调方法,该方法使用matplotlib绘制结果。

问题是,当我更快地(但不是不合理地快速)继续更改选项时,应用程序崩溃了。如果删除了线程,则不会发生这种情况。

我知道很多问题是由于在工作线程而不是主线程中发生绘图造成的,所以我相信我确保绘图只发生在主线程中。

我想问题的一部分是我可能误解了使用信号和插槽时的运行情况。我已经尝试在代码中的不同点找到正在使用的线程,但只能使用QThread.currentThread(),它返回地址并没有真正帮助,因为QThread.currentThreadId()导致此错误:AttributeError:type object 'PySide2.QtCore.QThread'没有属性'currentThreadId'。

我试图通过编写类似崩溃的应用程序的最小版本来隔离行为,其中大部分都放在下面。我已经排除了计算,因为我不确定我是否可以共享它,并且我已经使用带有几个选项的QListWidget替换了绘图选项。它需要更多的交互来崩溃,而不是正确的应用程序,在某些情况下,在一秒或两秒的空间中选择几个选项后崩溃,但希望说明这一点。

class MainWindow(QObject):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.main_window = QMainWindow()
        self.main_window.setCentralWidget(QWidget())
        self.main_window.centralWidget().setLayout(QHBoxLayout())
        self.setup_main_window()

    def setup_main_window(self):
        print(f'setup_main_window thread address: {QThread.currentThreadId()}')
        self.load_list()
        self.plot_figure = PlotFigure()
        self.canvas = FigureCanvas(self.plot_figure)
        self.plot_figure.plot(update=False)
        self.main_window.centralWidget().layout().addWidget(self.canvas)

    def load_list(self):
        self.order_list = QListWidget(self.main_window)
        self.list_items = [
            QListWidgetItem('1', self.order_list),
            QListWidgetItem('2', self.order_list),
            QListWidgetItem('3', self.order_list),
            QListWidgetItem('4', self.order_list),
        ]
        self.order_list.itemClicked.connect(self.order_list_item_changed)
        self.main_window.centralWidget().layout().addWidget(self.order_list)

    def order_list_item_changed(self):
        print(f'order_list_item_changed thread address: {QThread.currentThreadId()}')
        self.plot_figure.plot()

    def show(self):
        if self.main_window is not None:
            self.main_window.show()

class PlotFigure(Figure):
    def __init__(self):
        super().__init__()

    def plot(self):
        print(f'plot thread address: {QThread.currentThreadId()}')
        print(f'update: {update}')
        print(f'connecting signals')
        worker = Worker(self.calc)
        #worker.signals.close.connect(self.set_end_calc)
        worker.signals.finished.connect(self.plot_result)
        print(f'threads: {QThreadPool.globalInstance().activeThreadCount()}')
        QThreadPool.globalInstance().start(worker)

    def plot_result(self, m, xs, ys):
        print(f'plot_result thread address: {QThread.currentThreadId()}')
        print('plotting')
        fig = self.canvas.figure
        fig.clear()
        self.axis = fig.add_subplot(111)
        self.image = self.axis.imshow(m,
                origin='lower', 
                aspect='auto',
                cmap=matplotlib.cm.get_cmap('inferno'), 
                interpolation='bilinear',
                extent=(xs[0], xs[-1], ys[0], ys[-1])
        )
        self.canvas.draw()
class WorkerSignals(QtCore.QObject):
    close = QtCore.Signal(bool)
    start = QtCore.Signal(bool)
    finished = QtCore.Signal(list, list, list)

class Worker(QtCore.QRunnable):
    def __init__(self, worker_method):
        super(Worker, self).__init__()
        self.signals = WorkerSignals()
        self.worker_method = worker_method

    def run(self):
        self.signals.close.emit(True)
        print('close signal sent')
        m, xs, ys = self.worker_method()
        print('calc done')
        self.signals.finished.emit(m, xs, ys)

我应该能够选择新选项(单击列表小部件),从线程池启动一个新线程,运行计算并发回要绘制的结果。如果在短时间内选择了太多选项,应用程序将崩溃。当一切都发生在主线程中时,这不会发生。

任何人都可以告诉我为什么应用程序可能崩溃并提供解决方案来修复崩溃?

python multithreading matplotlib pyqt pyside2
1个回答
0
投票

您描述的崩溃行为指向多个线程同时尝试对相同的数据/变量进行操作。由于您没有限制可以旋转的其他线程的数量,如果用户快速连续触发多个工作线程,很可能会发生这种情况。

有几种方法可以解决这个问题,您选择的方法取决于您的要求。

Copy data

如果由于两个线程正在访问同一内存而发生崩溃,一个简单的解决方案是获取数据的副本。请注意,对于列表,dicts等,您需要deepcopy数据以确保嵌套值也被复制。对于大数据,这可能会导致一些开销。

你没有在你的例子中包含你的calc方法,所以我不能建议你如何在你的情况下这样做,但一般情况下。

from copy import copy
data = 'a simple string'
data_c = copy(data)

或者,深度复制

from copy import deepcopy
data = {'a':'dict', 'of':'items'}
data_c = deepcopy(data)

Worker locks

如果您有一个特定的工作者,一次只能运行一个,您可以实现原始锁。下面假设您希望最新的工作人员始终运行,并在新任务到达时丢弃任何早期工作人员。

self._worker_lock = False
self._worker_waiting = False

每个工人.finished信号应该连接到一个执行的方法。

def worker_finised(self):
    self._worker_lock = False
    if self._worker_waiting: 
        QThreadPool.globalInstance().start(self._worker_waiting)
        self._worker_waiting = False

这将在当前工作完成时自动启动下一个工作程序。如果您希望运行的所有工作人员都可以使用list队列。

最后,当启动工人时。

def plot()
    worker = Worker(self.calc)
    worker.signals.finished.connect(self.plot_result)

    if self._worker_lock:
        self._worker_waiting = worker
    else:
        self._worker_lock
        QThreadPool.globalInstance().start(worker)

Limit threads to 1

这是一种愚蠢的方法,它还可以防止您使用线程进行其他计算并使线程池无效。

threadpool = QThreadPool()
threadpool. setMaxThreadCount(1)

即使这样,如果draw和calc方法仍然可以同时执行,这可能还不够,你仍然会崩溃。但是如果你的线程只相互冲突就足够了。

© www.soinside.com 2019 - 2024. All rights reserved.