¿Por qué matplotlib no puede trazar en un hilo diferente?

Ejemplo de trabajo mínimo

Espero que lo siguiente muestre una ttwig, pero no veo ninguna ttwig y el intérprete simplemente se cuelga (mi backend se informa como 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' 

¿Cómo consigo la ttwig para mostrar?

Contexto

Estoy ejecutando una simulación con muchas iteraciones y me gustaría actualizar mi ttwig cada 1000 iteraciones para poder monitorear cómo está evolucionando mi simulación.

Psuedocode abajo:

 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 

Tener la ttwig en el hilo principal hace que la GUI de la ttwig se cuelgue o se bloquee, presumiblemente porque tengo otro trabajo en curso. Así que la idea era hacer el trazado en un hilo separado.

He visto sugerencias (por ejemplo, aquí ) para usar un proceso en lugar de un hilo. Pero luego no puedo manipular la figura o los ejes para agregar líneas, etc., mientras mi simulación se ejecuta porque el objeto figura está en el proceso remoto.

Editar

No estoy convencido de que esta pregunta sea un duplicado de otra porque esa pregunta se relaciona con el motivo por el cual la api de pyplot no se puede usar para manipular dos plots diferentes, cada una en un hilo diferente. Esto se debe a que las condiciones de carrera que surgen de la ejecución de dos plots simultáneamente impiden que pyplot determine qué figura es la cifra actual .

Sin embargo, solo tengo 1 plot y, por pyplot tanto, pyplot solo tiene una figura actual única y única.

Como otras personas han dicho, Matplotlib no es seguro para subprocesos, una opción que tienes es usar multiprocesamiento. Dice que esto no es bueno para usted, porque necesita acceder a los ejes de diferentes procesos, pero puede superar esto compartiendo datos entre el proceso de simulación y el proceso raíz y luego administrando todas las actividades relacionadas con el trazado en el proceso raíz. Por ejemplo

 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() 

La respuesta más simple probablemente es:

Porque los backends no son seguros para los hilos. La mayoría de los marcos de GUI se basan en llamar a los métodos / funciones de “GUI” desde un solo hilo (“hilo de gui”) y requieren métodos más avanzados cuando se comunican con diferentes hilos (“hilos de trabajo”).

Puede encontrar esto en la documentación de Qt (PyQt / PySide) , wxWidgets y (no encontró una fuente más oficial) Tkinter .

Tuve un problema similar en el que quería actualizar una gráfica de mapltolib desde un hilo diferente, y estoy publicando mi solución aquí en caso de que otros tengan un problema similar en el futuro.

Como se indicó, los tkagg no están seguros para los hilos, por lo que debe asegurarse de que todas las llamadas a matplotlib sean de un solo hilo. Esto significa que los subprocesos deben comunicarse, de modo que el ‘subproceso de trazado’ siempre ejecute funciones matplotlib.

Mi solución fue crear un decorador, que ejecutará todas las funciones decoradas en el ‘hilo de trazado’, y luego decorar todas las funciones relevantes. Esto le permite hacer lo que quiera sin ningún cambio en la syntax en el código principal.

es decir, cuando llama a ax.plot (…) en un subproceso, lo ejecutará automáticamente en un subproceso diferente.

 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() 

Tenga en cuenta que si olvida decorar alguna función, puede obtener un error de segmentación.

Además, en este ejemplo, el subproceso secundario controla la ttwig y el subproceso principal la simulación. En general, se recomienda hacer lo contrario, (es decir, dejar que el hilo principal tenga los gráficos).