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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/34764535/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-19 15:29:51  来源:igfitidea点击:

Why can't matplotlib plot in a different thread?

pythonmatplotlibplot

提问by mchen

Minimum working example

最小工作示例

I expect the following to show a plot, but i see no plot and the interpreter just hangs (my backend reports itself as TkAgg).

我希望以下内容显示一个情节,但我看不到情节,解释器只是挂起(我的后端报告自己为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'

How do I get the plot to display?

如何让绘图显示?

Context

语境

I am running a simulation with lots iterations and would like to update my plot every 1000 iterations so that I can monitor how my simulation is evolving.

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

Psuedocode below:

伪代码如下:

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

Having the plot in the main thread causes the plot GUI to hang/crash presumably because I have other work going on. So the idea was to do the plotting in a separate thread.

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

I have seen suggestions (e.g. here) to use a process rather than a thread. But then I cannot manipulate the figure or axes to add lines etc while my simulation runs because the figure object is in the remote process.

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

Edit

编辑

I'm not convinced this question is a duplicate of another onebecause that question deals with why the pyplotapi cannot be used to manipulate two different plotsthat are each on a separate thread. It is because race conditions arising from executing two plots simultaneously prevents pyplotfrom determining which figure is the current figure.

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

However, I only have 1 plot and so pyplotonly ever has a single and unique current figure.

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

采纳答案by Noel Segura Meraz

As other people have told, Matplotlib is not thread safe, one option you have is to use multiprocessing. You say that this is not good for you, because you need access to the axes from different process, but you can overcome this by sharing databetween the simulation process and the root process and then managing all the plotting related activities in the root process. For example

正如其他人所说,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()

回答by sebastian

The simplest answer probably is:

最简单的答案可能是:

Because the backends aren't thread safe. Most GUI frameworks rely on calling "GUI" methods/functions from only one thread ("gui thread") and require more advanced methods when communicating with different threads ("worker threads").

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

You can find this in the documentation for Qt (PyQt/PySide), wxWidgetsand (didn't find a more official source) Tkinter.

你可以在Qt (PyQt/PySide)wxWidgets和(没有找到更官方的来源)Tkinter的文档中找到这个。

回答by trygvrad

I had a similar problem where I wanted to update a mapltolib plot from a different thread, and I am posting my solution here in case others have a similar problem in the future.

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

As noted the tkagg are not threading safe so you must make sure all calls to matplotlib are from a single thread. This means that the threads must communicate, so that the 'plotting thread' always executes matplotlib functions.

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

My solution was to create a decorator, that will execute all decorated functions in the 'plotting thread', and then to decorate all the relevant functions. This allows you to do what you want without any change to syntax in the main code.

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

i.e. when you call ax.plot(...) in one thread, you will have it automatically executed in a different thread.

即当您在一个线程中调用 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()

Note that if you forget to decorate any functions, you may get a segmentation fault.

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

Also, in this example the child thread handles the plot and the main thread the simulation. In general it is advised to do the reverse, (i.e. let the main thread have the graphics).

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