【问题标题】:Why can't matplotlib plot in a different thread?为什么 matplotlib 不能在不同的线程中绘图?
【发布时间】:2016-04-18 07:12:16
【问题描述】:

最小工作示例

我希望下面显示一个情节,但我看不到情节并且解释器只是挂起(我的后端将自己报告为TkAgg)。

import matplotlib.pyplot as plt
from threading import Thread

def plot():
    fig, ax = plt.subplots()
    ax.plot([1,2,3], [1,2,3])
    plt.show()

def main():
    thread = Thread(target=plot)
    thread.setDaemon(True)
    thread.start()
    print 'Done'

如何让绘图显示?

上下文

我正在运行一个包含大量迭代的模拟,并且希望每 1000 次迭代更新一次我的绘图,以便我可以监控我的模拟是如何演变的。

下面的伪代码:

iterations = 100000
for i in iterations:
    result = simulate(iteration=i)
    if not i % 1000:
        # Update/redraw plot here:
        # Add some lines, add some points, reset axis limits, change some colours

将绘图放在主线程中会导致绘图 GUI 挂起/崩溃,大概是因为我还有其他工作正在进行。所以这个想法是在一个单独的线程中进行绘图。

我看到了使用进程而不是线程的建议(例如here)。但是在我的模拟运行时,我无法操纵图形或轴来添加线条等,因为图形对象处于远程进程中。

编辑

我不相信这个问题与another one 重复,因为该问题涉及为什么pyplot api 不能用于操纵两个不同的情节,每个情节都在一个单独的线程上.这是因为同时执行两个绘图产生的竞争条件会阻止pyplot 确定哪个图是当前图

但是,我只有 1 个情节,所以 pyplot 只有一个唯一的当前人物

【问题讨论】:

  • 创建一个用于处理图纸的流程(或脚本),另一个用于处理模拟。然后通过 IPC 连接它们。或者创建一个服务器客户端模型。服务器处理绘图,客户端处理模拟并提供服务器数据。
  • 在我的重复问题中没有关于 django 的具体内容,但基本上我遇到了同样的问题
  • Milo Chen,我建议将其作为副本,因为答案是相同的,matplotlib 并未设置为真正支持多线程,而是提供了允许多线程的 OO 接口模块
  • @OP 本身不是问题的答案,但您可以通过在主线程中绘制并将计算移至工作线程来解决您的问题。

标签: python matplotlib plot


【解决方案1】:

正如其他人所说,Matplotlib is not thread safe,您可以选择使用多处理。您说这对您不利,因为您需要访问来自不同进程的轴,但是您可以通过在模拟进程和根进程之间共享 数据 然后管理所有绘图来克服这个问题根进程中的相关活动。例如

import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
import multiprocessing
import time
import random
from Tkinter import *


#Create a window
window=Tk()



def main():
    #Create a queue to share data between process
    q = multiprocessing.Queue()

    #Create and start the simulation process
    simulate=multiprocessing.Process(None,simulation,args=(q,))
    simulate.start()

    #Create the base plot
    plot()

    #Call a function to update the plot when there is new data
    updateplot(q)

    window.mainloop()
    print 'Done'


def plot():    #Function to create the base plot, make sure to make global the lines, axes, canvas and any part that you would want to update later

    global line,ax,canvas
    fig = matplotlib.figure.Figure()
    ax = fig.add_subplot(1,1,1)
    canvas = FigureCanvasTkAgg(fig, master=window)
    canvas.show()
    canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
    canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=1)
    line, = ax.plot([1,2,3], [1,2,10])




def updateplot(q):
    try:       #Try to check if there is data in the queue
        result=q.get_nowait()

        if result !='Q':
             print result
                 #here get crazy with the plotting, you have access to all the global variables that you defined in the plot function, and have the data that the simulation sent.
             line.set_ydata([1,result,10])
             ax.draw_artist(line)
             canvas.draw()
             window.after(500,updateplot,q)
        else:
             print 'done'
    except:
        print "empty"
        window.after(500,updateplot,q)


def simulation(q):
    iterations = xrange(100)
    for i in iterations:
        if not i % 10:
            time.sleep(1)
                #here send any data you want to send to the other process, can be any pickable object
            q.put(random.randint(1,10))
    q.put('Q')

if __name__ == '__main__':
    main()

【讨论】:

  • 这是一个非常好的主意,但这里有一个大问题。如果计算比绘图过程更快会发生什么?队列将被填满,但 updatePlot 无法赶上(即使 window.after 设置为 0。
  • 这只是关于如何同时使用多处理和 matplotlib 的基本示例。您提出的案例很容易解决,您可以在每次迭代中获取队列的所有元素,并确保您的绘图函数每次都可以处理大块数据。在最坏的情况下,您会看到明显的跳跃和非常低的帧速率。但这只有在您的数据非常快或绘图非常缓慢时才会发生。无论哪种方式,如果这是您的问题,我建议尝试一些更复杂和/或面向实时的东西,例如 pygame
  • 您还可以查看 PyQt(围绕 C++ Qt 库的 Python 文件),它或多或少是为强大的实时可视化而构建的,具有通过小部件使 polt 交互的选项。跨度>
  • 是不是可腌制不可腌制
【解决方案2】:

我有一个类似的问题,我想从不同的线程更新 mapltolib 图,我在这里发布我的解决方案,以防其他人将来遇到类似的问题。

如前所述,tkagg 不是线程安全的,因此您必须确保对 matplotlib 的所有调用都来自单个线程。这意味着线程必须进行通信,以便“绘图线程”始终执行 matplotlib 函数。

我的解决方案是创建一个装饰器,它将执行“绘图线程”中的所有装饰功能,然后装饰所有相关功能。这使您可以在不更改主代码中的语法的情况下做您想做的事情。

即当您在一个线程中调用 ax.plot(...) 时,您将在另一个线程中自动执行它。

import matplotlib.pyplot as plt
import matplotlib
import threading
import time
import queue
import functools


#ript(Run In Plotting Thread) decorator
def ript(function):
    def ript_this(*args, **kwargs):
        global send_queue, return_queue, plot_thread
        if threading.currentThread() == plot_thread: #if called from the plotting thread -> execute
            return function(*args, **kwargs)
        else: #if called from a diffrent thread -> send function to queue
            send_queue.put(functools.partial(function, *args, **kwargs))
            return_parameters = return_queue.get(True) # blocking (wait for return value)
            return return_parameters
    return ript_this

#list functions in matplotlib you will use
functions_to_decorate = [[matplotlib.axes.Axes,'plot'],
                         [matplotlib.figure.Figure,'savefig'],
                         [matplotlib.backends.backend_tkagg.FigureCanvasTkAgg,'draw'],
                         ]
#add the decorator to the functions
for function in functions_to_decorate:
    setattr(function[0], function[1], ript(getattr(function[0], function[1])))

# function that checks the send_queue and executes any functions found
def update_figure(window, send_queue, return_queue):
    try:
        callback = send_queue.get(False)  # get function from queue, false=doesn't block
        return_parameters = callback() # run function from queue
        return_queue.put(return_parameters)
    except:
        None
    window.after(10, update_figure, window, send_queue, return_queue)

# function to start plot thread
def plot():
    # we use these global variables because we need to access them from within the decorator
    global plot_thread, send_queue, return_queue
    return_queue = queue.Queue()
    send_queue = queue.Queue()
    plot_thread=threading.currentThread()
    # we use these global variables because we need to access them from the main thread
    global ax, fig
    fig, ax = plt.subplots()
    # we need the matplotlib window in order to access the main loop
    window=plt.get_current_fig_manager().window
    # we use window.after to check the queue periodically
    window.after(10, update_figure, window, send_queue, return_queue)
    # we start the main loop with plt.plot()
    plt.show()


def main():
    #start the plot and open the window
    thread = threading.Thread(target=plot)
    thread.setDaemon(True)
    thread.start()
    time.sleep(1) #we need the other thread to set 'fig' and 'ax' before we continue
    #run the simulation and add things to the plot
    global ax, fig
    for i in range(10):
        ax.plot([1,i+1], [1,(i+1)**0.5])
        fig.canvas.draw()
        fig.savefig('updated_figure.png')
        time.sleep(1)
    print('Done')
    thread.join() #wait for user to close window
main()

请注意,如果您忘记装饰任何函数,您可能会遇到分段错误。

此外,在此示例中,子线程处理绘图,主线程处理模拟。一般来说,建议做相反的事情,(即让主线程有图形)。

【讨论】:

  • 这是一个非常有趣的方法,但它不适用于其他后端,例如 Qt5 :-/
【解决方案3】:

最简单的答案可能是:

因为后端不是线程安全的。大多数 GUI 框架依赖于仅从一个线程(“gui 线程”)调用“GUI”方法/函数,并且在与不同线程(“工作线程”)通信时需要更高级的方法。

您可以在Qt (PyQt/PySide)wxWidgets 和(未找到更官方的来源)Tkinter 的文档中找到此内容。

【讨论】:

    【解决方案4】:

    我想出了一个解决方案。不需要其他 UI 库。

    以下源代码会打开一个 matplotlib 窗口,其画布会定期更新。

    import time
    import _thread
    import matplotlib.pyplot as plt
    import numpy as np
    
    def plotting_thread(fig, axe):
        while (True):
            mat = np.random.randn(256, 256)
            time.sleep(2)  # ... or some busy computing
            axe.clear()
            axe.imshow(mat)
            fig.canvas.draw_idle()  # use draw_idle instead of draw
    
    fig = plt.figure()  # the figure will be reused later
    axe = fig.add_subplot(111)
    
    _thread.start_new_thread(plotting_thread, (fig, axe))
    
    plt.show()
    

    三个键

    • 启动一个新的计算线程
    • 使用canvas.draw_idle 更新您的绘图,从而保持窗口的响应
    • 在程序末尾使用plt.show 来启动GUI 主事件循环,这将阻塞该程序。所以在那之后不要写代码。

    【讨论】:

    • 如果 plt.show() 必须在主线程结束时调用,这样的例子怎么可能有用?可以在其他线程中启动 main 吗?
    • 是的,它现在必须弄清楚如何将值从主线程传递到绘图
    • 读取线程shafe sa.e memory spacd 所以不需要管道/流任何东西?
    【解决方案5】:

    Maplotlib 不是线程安全的。对我来说,尝试使用 tkinter 和 matplotlib 自动实时更新图形是相当具有挑战性的。

    我的解决方法是使用 root.after 函数 (tkinter: how to use after method)

    自动实时更新,无需单独的线程 :)

    也解决了单独线程调用canvas.draw时tkinter无错误崩溃的问题。

    【讨论】:

      【解决方案6】:

      答案取在这里:https://matplotlib.org/stable/gallery/misc/multiprocess_sgskip.html我尝试了几种方法,但这是唯一适合我的方法。

          """
          Written by Robert Cimrman
          """
      
          import multiprocessing as mp
          import time
      
          import matplotlib.pyplot as plt
          import numpy as np
      
          np.random.seed(19680801)
      
          class ProcessPlotter(object):
              def __init__(self):
                  self.x = []
                  self.y = []
      
              def terminate(self):
                  plt.close('all')
      
              def call_back(self):
                  while self.pipe.poll():
                      command = self.pipe.recv()
                      if command is None:
                          self.terminate()
                          return False
                      else:
                          self.x.append(command[0])
                          self.y.append(command[1])
                          self.ax.plot(self.x, self.y, 'ro')
                      self.fig.canvas.draw()
                  return True
      
              def __call__(self, pipe):
                  print('starting plotter...')
      
                  self.pipe = pipe
                  self.fig, self.ax = plt.subplots()
                  timer = self.fig.canvas.new_timer(interval=1000)
                  timer.add_callback(self.call_back)
                  timer.start()
      
                  print('...done')
                  plt.show()
      
          class NBPlot(object):
              def __init__(self):
                  self.plot_pipe, plotter_pipe = mp.Pipe()
                  self.plotter = ProcessPlotter()
                  self.plot_process = mp.Process(
                      target=self.plotter, args=(plotter_pipe,), daemon=True)
                  self.plot_process.start()
      
              def plot(self, finished=False):
                  send = self.plot_pipe.send
                  if finished:
                      send(None)
                  else:
                      data = np.random.random(2)
                      send(data)
      
      
          def main():
              pl = NBPlot()
              for ii in range(10):
                  pl.plot()
                  time.sleep(0.5)
              pl.plot(finished=True)
      
      
          if __name__ == '__main__':
              if plt.get_backend() == "MacOSX":
                  mp.set_start_method("forkserver")
              main()
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-01-11
        • 1970-01-01
        • 2015-10-05
        • 2016-11-26
        • 1970-01-01
        • 1970-01-01
        • 2020-07-17
        • 1970-01-01
        相关资源
        最近更新 更多