You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 01:54:52