matplotlib.pyplot.hist多线程运行过慢及线程安全问题咨询
问题描述
我用Tkinter搭建了相机控制前端面板,为保证控制循环的响应性,创建了ImageProcessor类负责图像采集、处理及基于cv2的绘图,该类实例运行在独立线程中。由于Tkinter非线程安全,我采用Agg作为Matplotlib后端,将pyplot.figure的画布通过cv2绘制。但使用pyplot.hist生成图像直方图时耗时超20秒,单独计算直方图仅需0.5秒。
请问该现象的原因是什么?Matplotlib是否必须在主线程运行,还是仅需单个线程与之交互即可(如我的场景)?或是我的代码存在其他误区?
附原代码:
import threading import time import numpy as np import matplotlib import matplotlib.pyplot as plt from timeit import default_timer as timer from datetime import timedelta import queue class ImageProcessor(threading.Thread): def __init__(self): matplotlib.use('Agg') threading.Thread.__init__(self) # initialize plot for histograms self.hist_fig = plt.figure() self.loop = True self.continuous_acquisition_var = False self.a = None def run(self): while self.loop: self.a = np.random.uniform(low=0, high=16384, size=12320768).reshape((4096, 3008)) self.hist_fig.clf() # clear histogram plot start = timer() plt.hist(self.a.flatten(), bins=256, range=(0.0, 16384), fc='r', ec='r') end = timer() print(timedelta(seconds=end - start)) def stop(self): self.loop = False def ctl_loop(command): ctl_loop_var = True img_proc = ImageProcessor() img_proc.daemon = True img_proc.start() while ctl_loop_var: # main loop while not command.empty(): q_element = command.get() task = q_element[0] data = q_element[1] func = getattr(img_proc, task) func(data) if task == "stop": ctl_loop_var = False if __name__ == '__main__': cmd_queue = queue.Queue() ctl = threading.Thread(target=ctl_loop, args=(cmd_queue, )) ctl.daemon = True ctl.start() time.sleep(40) cmd_queue.put(('stop', ''))
问题分析与解决方案
1. 耗时超20秒的核心原因
性能问题来自两个关键误区:
- 误用Matplotlib状态机接口:
plt.hist属于pyplot全局状态机API,依赖全局的"当前Figure/轴"上下文。你已创建self.hist_fig,但每次调用plt.hist时,pyplot会隐式与全局状态交互,甚至重复创建资源,带来巨大额外开销;而单独计算直方图(如np.histogram)是纯数值计算,无此类开销。 - 频繁销毁重建Axes:每次循环调用
self.hist_fig.clf()清除整个Figure,再用plt.hist重新创建Axes,反复创建销毁的操作大幅降低了性能。
2. Matplotlib线程安全说明
Matplotlib不强制要求必须在主线程运行,但需遵守两个规则:
- 所有针对同一个Figure/Axes对象的操作,必须在同一个线程中完成(你的场景中所有Matplotlib操作都在ImageProcessor线程,符合要求)。
- 避免使用pyplot全局状态机API(
plt.xxx),改用面向对象API直接操作Figure和Axes,彻底规避全局状态带来的线程安全问题和性能损耗。
另外,matplotlib.use('Agg')的调用位置错误:它必须在导入任何Matplotlib模块(包括plt)之前执行,否则后端初始化后再修改会无效,甚至引发隐式冲突。
3. 代码优化方案
修改后的核心代码如下,主要改动为改用面向对象API操作Axes、复用资源、修正后端设置位置:
import threading import time import numpy as np import matplotlib # 提前设置后端,必须在导入plt之前 matplotlib.use('Agg') import matplotlib.pyplot as plt from timeit import default_timer as timer from datetime import timedelta import queue class ImageProcessor(threading.Thread): def __init__(self): threading.Thread.__init__(self) # 初始化Figure和Axes,复用同一个Axes self.hist_fig = plt.figure() self.hist_ax = self.hist_fig.add_subplot() self.loop = True self.continuous_acquisition_var = False self.a = None def run(self): while self.loop: self.a = np.random.uniform(low=0, high=16384, size=12320768).reshape((4096, 3008)) # 只清除Axes内容,不销毁Axes self.hist_ax.clear() start = timer() # 直接用Axes对象调用hist,避免全局状态交互 self.hist_ax.hist(self.a.flatten(), bins=256, range=(0.0, 16384), fc='r', ec='r') end = timer() print(timedelta(seconds=end - start)) def stop(self): self.loop = False def ctl_loop(command): ctl_loop_var = True img_proc = ImageProcessor() img_proc.daemon = True img_proc.start() while ctl_loop_var: # main loop while not command.empty(): q_element = command.get() task = q_element[0] data = q_element[1] func = getattr(img_proc, task) func(data) if task == "stop": ctl_loop_var = False if __name__ == '__main__': cmd_queue = queue.Queue() ctl = threading.Thread(target=ctl_loop, args=(cmd_queue, )) ctl.daemon = True ctl.start() time.sleep(40) cmd_queue.put(('stop', ''))
优化效果说明
- 改用
self.hist_ax.hist直接操作Axes,彻底避开pyplot全局状态的额外开销,耗时会大幅降低至接近纯数值计算的水平。 - 复用同一个Axes对象,仅清除内容不销毁,避免了反复创建Axes的资源消耗。
- 修正
matplotlib.use('Agg')的位置,确保后端正确初始化。
内容的提问来源于stack exchange,提问作者JMP
相关产品推荐
相关产品推荐

