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

时间:2019-02-14 14:50:01

标签: python multithreading matplotlib pyqt pyside2

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

问题在于,当我连续(而不是不合理地快速)更改选项的选择时,应用崩溃。如果删除线程,则不会发生这种情况。

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

我想部分问题是,使用信号和插槽时,我可能会误解正在运行的地方。我尝试查找在代码的不同点使用了什么线程,但是只能使用QThread.currentThread(),它返回地址,并且对QThread.currentThreadId()导致此错误没有太大帮助:AttributeError:类型对象“ 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)

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

有人可以告诉我为什么该应用可能崩溃,并提供解决崩溃的解决方案吗?

1 个答案:

答案 0 :(得分:0)

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

有几种处理方法,具体取决于您的要求。

复制数据

如果由于两个线程正在访问相同的内存而发生崩溃,则简单的解决方案是获取数据的副本。请注意,对于列表,字典等,您将需要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)

工人锁

如果您有一个特定的工作程序,一次只能运行一个工作程序,则可以实现原始锁。下面假设您希望 latest 工作程序始终运行,并且在新任务到达时将丢弃所有较早的工作程序。

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)

将线程限制为1

这是一种愚蠢的方法,它还会阻止您将线程用于其他计算,并使线程池变得无用。

threadpool = QThreadPool()
threadpool. setMaxThreadCount(1)

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