为什么matplotlib不能在不同的线程中绘图?

mch*_*hen 21 python plot matplotlib

最低工作示例

我希望以下内容能够显示一个情节,但我看不到情节,而且解释器只是悬挂(我的后端报告自己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'
Run Code Online (Sandbox Code Playgroud)

如何显示情节?

上下文

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

Psuedocode如下:

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
Run Code Online (Sandbox Code Playgroud)

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

我已经看到使用进程而不是线程的建议(例如这里).但是当我的模拟运行时,我无法操纵图形或轴来添加线等,因为图形对象在远程过程中.

编辑

我不相信这个问题是另一个问题的重复,因为这个问题涉及为什么pyplotapi不能用于操纵两个不同的图,每个都在一个单独的线程上.这是因为同时执行两个图所产生的竞争条件阻止pyplot了确定哪个数字是当前数字.

但是,我只有1个情节,因此pyplot只有一个独特的电流数字.

Noe*_*raz 15

正如其他人所说,Matplotlib不是线程安全的,你有一个选择就是使用多处理.您说这对您不利,因为您需要从不同的流程访问轴,但您可以通过在模拟流程和根流程之间共享数据,然后管理根流程中的所有绘图相关活动来克服这一点.例如

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()
Run Code Online (Sandbox Code Playgroud)


小智 5

我遇到了类似的问题,我想从不同的线程更新 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()
Run Code Online (Sandbox Code Playgroud)

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

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


Bob*_*een 5

我想出了一个解决方案。不需要其他 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()
Run Code Online (Sandbox Code Playgroud)

三个键

  • 启动新线程进行计算
  • 用于canvas.draw_idle更新绘图,以保留窗口的响应
  • plt.show在程序末尾使用来启动GUI主事件循环,这将阻止该程序。所以之后就不要写代码了。