使用Matplotlib、PyQt和线程进行实时绘图可能导致Python崩溃。

3

我一直在与我的Python应用程序苦苦挣扎,但找不到任何答案。

我有一个使用Matplotlib小部件的PyQT GUI应用程序。GUI启动了一个新线程来处理对mpl小部件的绘图。我担心从另一个线程访问matplotlib绘图组件会导致竞争条件而崩溃。

这基本上就是我的代码的样子:

class Analyzer(QMainWindow, Ui_MainWindow):
  def __init__(self, parent=None):
    self.timer = QTimer()
    super(Analyzer, self).__init__(parent)
    self.setupUi(self)

    self.background = self.mpl.canvas.copy_from_bbox(self.mpl.canvas.ax.bbox)

    self.plotQueue = Queue.Queue()
    self.plotterStarted = False

    self.plotter = Plotter(self.mpl, self.plotQueue)
    self.cam = Cam(self.plotQueue, self.textEdit)
    ...

class Ui_MainWindow(object):
  def setupUi(self, MainWindow):
    ...
    self.mpl = MplWidget(self.centralWidget)
    ...

class MplWidget(QtGui.QWidget):
"""Widget defined in Qt Designer"""
  def __init__(self, parent = None):
    QtGui.QWidget.__init__(self, parent)
    self.canvas = MplCanvas()
    ...

class MplCanvas(FigureCanvas):
"""Class to represent the FigureCanvas widget"""
  def __init__(self):        
    # setup Matplotlib Figure and Axis
    self.fig = Figure()
    self.ax = self.fig.add_subplot(111)

    # initialization of the canvas
    FigureCanvas.__init__(self, self.fig)

    FigureCanvas.updateGeometry(self)

绘图机类:

class Plotter():
  def __init__(self, mpl="", plotQueue=""):
    self.mpl = mpl
    self.background = self.mpl.canvas.copy_from_bbox(self.mpl.canvas.ax.bbox)
    self.plotQueue = plotQueue
    ...
  def start(self):
    threading.Thread(target=self.run).start()

  ''' Real time plotting '''
  def run(self):
    while True:
      try:
        inputData = self.plotQueue.get(timeout=1)

        # Go through samples
        for samples in inputData:
            self.line, = self.mpl.canvas.ax.plot(x, y, animated=True, label='Jee')

            for sample in samples:
                x.append(sample['tick'])
                y.append(sample['linear'])

            self.line.set_data(x,y)
            self.mpl.canvas.ax.draw_artist(self.line)
            self.mpl.canvas.blit(self.mpl.canvas.ax.bbox)
         ...

我将mpl和plotQueue传递给Plotter类对象。PlotQueue是在Cam类中填充的,该类处理来自外部硬件的输入数据。Plotter读取plotQueue,对其进行处理并调用mpl的绘图功能。
但是这种访问mpl的方法是否线程安全?如果不是,我应该怎么做?对此有任何建议都将不胜感激。
编辑1:
我在主线程中添加了QTimer来处理绘图,如评论中所建议的。经过一些微调,我使其工作得相当好。
class Analyzer(...):
  def __init__(self, parent=None):
    QObject.connect(self.timer, SIGNAL("timeout()"), self.periodicCall)

  def periodicCall(self):
    self.plotter.draw()

  def startButton(self):
    self.timer.start(10)

非常感谢您提供有用的意见。

我记得读到过关于matplotlib在多线程方面存在问题的内容。大多数绘图命令应该非常快(并且您可以使用blitting来解决大多数缓慢的情况),请在主线程中执行它们。 - tacaswell
谢谢,这也是我一直在考虑的,但在这种情况下,我的主线程位于实现GUI的Analyzer类中。Analyzer类有按钮等,因此如果我在那里创建一个循环来读取队列并调用绘图,它会阻塞整个应用程序。我如何能在主线程中进行循环?我认为必须使用另一个线程进行绘图,但如何进行线程安全?我可以使用同步,但除了我的绘图方法之外,在哪里应该放置其他同步调用,它应该在matplotlib库的某个地方。 - PyTechnic
1个回答

1
如果你的程序中使用了QT后端的matplotlib(我假设你是这样做的,因为你将其嵌入到了Qt应用程序中),那么绘图将在调用matplotlib命令的线程中完成。这会是个问题,因为Qt要求所有绘图都必须在主线程中完成。所以我相当确定你不能简单地修复它。(如果你使用GTK,你可以使用gtk锁来防止主进程在你从线程中进行GUI相关操作时与GUI交互,但Qt在v4及以上版本中取消了它们类似的锁)。
你有几个选择:
  1. 尝试将matplotlib的绘图部分(可能不可能)分离出来,并通过使用QApplication.postEvent()发送事件使其在主线程中运行。

  2. 不要使用线程,只需在主线程中使用回调函数(可以使用QTimer定期调用或程序处于空闲状态时调用)。由于Python GIL阻止了真正的多线程行为,因此这可能不会影响您的应用程序性能。

  3. 使用不同的绘图库。我最近看了一下PyQtGraph,它似乎发展得很好。从我短暂的浏览中,我认为它有能力为您处理所有这些后台工作,使用RemoteGraphicsView。这将启动第二个进程来执行CPU密集型的绘图工作,避免了前面提到的Python GIL问题。如果您感兴趣,请查看他们提供的示例。


非常感谢。选项1是我在之前的评论中尝试解决的问题,但我相信那不是正确的方法。选项2可能适合我。如果库能提供我所需的功能,我也会检查选项3。 - PyTechnic
或者,如果我在主线程中有一个回调方法来进行绘图。然后我从线程中调用此方法并传递所需的参数。因此,基本上,我只是使用线程而不是定时器来调用该方法? - PyTechnic
这样做行不通,因为你调用的方法将在线程中执行,而不是主线程中执行。你需要使用QApplication.postEvent()使函数在主线程中运行。实际上这很复杂,我会尽快为您提供代码(只需确认编写代码的人是否愿意发布)。 - three_pineapples
不,那并不是必要的。我会选择定时器,因为它似乎是一个有效的方法来处理这种事情。绘图并不像在其他线程中处理数据那样关键。 - PyTechnic
尝试将QTimer的超时设置为0。这将使回调在GUI线程处于空闲状态时运行。您可能还希望修改代码,以便: A)不会阻塞尝试从队列中获取某些内容(假设您的timeout=1,则如果队列中没有任何内容,则会阻塞1秒) B)获取队列中的所有内容,而不仅仅是每次一个项目。 - three_pineapples
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接