Python多线程实现Matplotlib多幅实时图表绘制问题求解
问题原因梳理
- 线程创建参数错误:初始代码给
threading.Thread的target参数传入的是加了括号的函数调用(如thread_fgn_9()),会直接在主线程同步执行该死循环函数,子线程根本没有被创建启动,第一个函数跑起来就阻塞了后续所有逻辑。 - Matplotlib线程安全问题:Matplotlib的GUI渲染、画布/坐标轴操作本身不是线程安全的,官方要求所有绘图相关操作必须在主线程执行,你在子线程里直接操作
ax.plot、plt.pause会产生数据竞争,引发递归错误、卡顿、崩溃等异常。 - 多进程方案不可行:进程拥有独立内存空间,主进程创建的
fig、ax对象无法共享给子进程,子进程操作的只是父进程对象的拷贝,和主进程显示的画布完全无关,就算加锁也无法实现多进程操作同一张画布。
正确实现方案
采用「子线程负责数据计算 + 队列做线程间通信 + 主线程统一绘图更新」的架构,完全规避线程安全问题,步骤如下:
- 定义两个队列分别接收两个子线程计算出来的采样数据
- 子线程只做MBM采样计算,把结果扔到对应队列,不碰任何绘图相关代码
- 主线程用
matplotlib.animation.FuncAnimation定期从队列取数据,更新已有曲线的坐标值,重绘画布(不用每次调用ax.plot新建曲线,大幅提升绘图性能) - 如果需要长期运行避免内存占用过高、渲染变慢,可以在更新数据时限制历史数据长度,只保留最近N个采样点即可。
完整可运行代码
from fbm import MBM import matplotlib.pyplot as plt import threading import queue from matplotlib.animation import FuncAnimation plt.style.use('ggplot') fig, ax = plt.subplots(nrows=2, ncols=1, figsize=(8,6)) # 初始化两条空曲线,后续直接更新数据 line9, = ax[0].plot([], [], "g-o") line75, = ax[1].plot([], [], "r-o") # 初始化y轴范围,可根据实际采样值调整 ax[0].set_ylim(-2, 2) ax[1].set_ylim(-2, 2) ax[0].set_title("Hurst=0.9") ax[1].set_title("Hurst=0.75") # 定义两个队列存储子线程的计算结果 q9 = queue.Queue() q75 = queue.Queue() def h_90(t): return 0.9 def h_75(t): return 0.75 # 子线程仅做数据计算,不操作任何绘图对象 def calc_fgn_9(): i = 1 while True: f = MBM(n=1, hurst=h_90, length=1, method='riemannliouville') fgn_sample = f.mgn() q9.put((i, fgn_sample[0])) i += 1 # 控制计算频率和绘图频率匹配 plt.pause(0.1) def calc_fgn_75(): i = 1 while True: f = MBM(n=1, hurst=h_75, length=1, method='riemannliouville') fgn_sample = f.mgn() q75.put((i, fgn_sample[0])) i += 1 plt.pause(0.2) # 存储历史采样数据 x9, y9 = [], [] x75, y75 = [], [] # 动画更新函数,仅在主线程执行,负责所有绘图操作 def update(frame): # 取出队列中所有新生成的采样数据 while not q9.empty(): i, val = q9.get() x9.append(i) y9.append(val) # 可选:限制仅保留最近100个点,提升流畅度 # if len(x9) > 100: # x9.pop(0) # y9.pop(0) while not q75.empty(): i, val = q75.get() x75.append(i) y75.append(val) # 更新曲线数据 line9.set_data(x9, y9) line75.set_data(x75, y75) # 动态调整x轴范围 if x9: ax[0].set_xlim(0, max(x9)+1) if x75: ax[1].set_xlim(0, max(x75)+1) return line9, line75 if __name__ == "__main__": # 启动计算子线程 t1 = threading.Thread(target=calc_fgn_9, daemon=True) t2 = threading.Thread(target=calc_fgn_75, daemon=True) t1.start() t2.start() # 启动动画,主线程负责渲染 ani = FuncAnimation(fig, update, interval=100, blit=True) plt.tight_layout() plt.show()
内容的提问来源于stack exchange,提问作者Ben Eslami
相关产品推荐
相关产品推荐

