【问题标题】:Python 3 Tkinter Treeview Slow Performance while MultithreadedPython 3 Tkinter Treeview 多线程时性能缓慢
【发布时间】:2021-03-09 15:01:35
【问题描述】:

Tl;dr:从另一个线程向 Treeview 小部件插入 1500 次需要 40-340 秒才能完成,但如果在主线程上或根窗口很小时插入,则只需 1.2-1.7 秒即可完成看不到 Treeview。

我正在从事一个涉及识别游戏图像的神经网络的小项目,我试图弄清楚为什么我的结果 Treeview 在识别图像后更新如此缓慢。我下面的代码是我遇到的一般问题的 MCVE。

我的应用程序当前训练网络,根据训练模型进行预测,然后在显示这些预测结果时,显示 1500 个结果所需的时间比训练和预测所需的时间要长。在我的应用程序和这个例子中,我发现通常填充按钮完成和显示所有结果所需的时间似乎与窗口的高度(或 Treeview 小部件的高度)成正比,在这个例子中在 40 到 340 秒之间显示我当前机器上的所有 1500 行输出。我尝试使用 cProfile 和 pstats 来确定更具体地导致延迟的原因,但是我仍然对它们缺乏经验,尽管我知道大约 99% 的时间都花在了 '_tkinter.tkapp' 的 '{method'call' 上objects}' 和 '{method 'globalsetvar' of '_tkinter.tkapp' objects}',我不知道这些是什么,也不知道如何处理这些信息。

但是,我发现如果在窗口很小以至于无法显示 Treeview 的情况下启动辅助功能,则显示所有结果大约需要 1.2 - 1.7 秒。这可以通过观察我的示例中的进度条来明显地看到,并且可以看到它的进度越快,窗口越小。由于它可以在这段时间内显示所有结果,这表明(至少对我而言)插入结果时树视图可见的绝大多数时间都花在了一遍又一遍地渲染文本和更新高度上的滚动条。为此,我一直在尝试找到一种方法来一次插入大量行,或者至少在每次更改后不重新渲染 Treeview,但还没有找到任何似乎能够做到这一点的方法。

在尝试创建此 MCVE 时,我发现如果我在主线程上调用 add_entries 函数而不是在另一个线程上调用它,则显示所有结果需要同样短的时间(约 1.5 秒)。虽然我认为这可能是一个可行的解决方案,但我很好奇是否有更好的解决方案,同时尝试获取有关我迄今为止很难找到的问题的更多信息。

到目前为止,我发现的最接近的是 this discussion 谈论如何使用类似模块 (GTK3) 有人遇到类似问题,解决方案是将 Treeview 设置为固定高度模型,但我找不到任何关于常规 tkinter Treeview 小部件可用的类似选项的信息,而不仅仅是 GTK3,并且很好奇 tkinter 中是否存在这样的选项,或者它是否是其他模块独有的,或者是否有更好的解决方案我还没有完全想到了。

    import random, threading
    import cProfile, pstats
    from tkinter import *
    from tkinter.font import Font
    from tkinter.ttk import Progressbar, Treeview
    from string import printable
    
    COLS = list(range(10))
    ROWS = 1500
    
    def __main__():
        root = Tk()
        program_window = App(root)
        try:
            root.destroy()
        except TclError:
            pass
    
    class App(Frame):
        def __init__(self, parent=None):
            self.parent = parent
            
            self.bar_int = IntVar(value=0)
            self.tree = Treeview(self.parent, columns=COLS, show="headings")
            self.vsb = Scrollbar(self.parent, orient='vertical', command=self.tree.yview)
            self.bar = Progressbar(self.parent, variable=self.bar_int)
            self.btn = Button(self.parent, text="Populate", command=self.add_entries)
            
            for col in COLS:
                self.tree.heading(col, text=str(col))
                self.tree.column(col, width=Font().measure(str(col)))
            
            self.tree.grid(row=0, column=0, sticky='nsew', columnspan=2)
            self.vsb. grid(row=0, column=2, sticky='nsew')
            self.bar. grid(row=1, column=0, sticky='nsew')
            self.btn. grid(row=1, column=1, sticky='nsew', columnspan=2)
            
            self.parent.columnconfigure(0, weight=1)
            self.parent.rowconfigure(0, weight=1)
            self.parent.geometry('300x200')
            
            self.parent.mainloop()
        
        def add_entries(self):
            worker = threading.Thread(target=self.add_entries_worker)
            worker.start()
        
        def add_entries_worker(self):
            self.tree.delete(*self.tree.get_children())
            self.bar.configure(maximum=ROWS)
            with cProfile.Profile() as profile:
                for i in range(ROWS):
                    self.bar_int.set(i)
                    li = [random.sample(printable, 10) for i in COLS]
                    self.tree.insert('', 'end', values=li)
                ps = pstats.Stats(profile)
                ps.print_stats()
    
    if __name__ == "__main__":
        __main__()

更新:在 hussic 的建议下,在与朋友讨论了这个问题后,我研究了 root.after() 和 Queue。在 hussic 的建议下,我能够让我当前的代码在一定程度上工作,但它似乎仍然不稳定,我不确定我是否已经完成了我应该做的所有事情。下面是我添加的新 refresher() 函数,以及对 add_entries() 函数的修改。 add_entries_worker() 只是删除了对 tkinter 小部件的所有引用,并将其插入树替换为附加到 self.data_queue。

    def refresher(self):
        # while loop so I can pop data out of the list one at a time rather than
        # for loop and potentially delete unprocessed data when clearing the list
        while self.data_queue:
            i, item = self.data_queue.pop(0)
            self.bar_int.set(i)
            self.tree.insert('', 'end', values=item)
        
        if self.btn['state'] == 'disabled':
            self.parent.after(100, self.refresher)
    
    def add_entries(self):
        self.btn.configure(state=DISABLED)
        self.tree.delete(*self.tree.get_children())
        self.bar.configure(maximum=ROWS)
        self.refresher()
        
        worker = threading.Thread(target=self.add_entries_worker)
        worker.start()

【问题讨论】:

  • 你不应该从另一个线程调用 tkinter func。从主线程调用的同一个工作线程要快 10 倍。
  • 我经常在示例代码中看到许多其他使用多线程 tkinter 函数的应用程序。特别是考虑到我正在尝试训练神经网络并识别图像,我宁愿不在主线程上调用这些函数,而是让它在处理所有内容时挂起 10-40 秒。据我所知,防止主窗口长时间挂起的方法是在另一个线程上调用这些其他函数。除了让窗口挂起无响应之外,我真的没有其他办法来解决这个问题吗?
  • 单独的线程应该进行大量计算,但只有主线程应该更新 tkinter 窗口。您应该使用 Queue 将数据从辅助线程传递到主线程。您还应该使用块和 root.after() 更新树视图。
  • 我目前正在研究 root.after() 并且能够获得您所说的工作的基本版本,但它似乎并不稳定。我已经让它崩溃了两次,关于一个函数在主线程之外被终止的事情,但是我从那以后就无法复制它,并且我的控制台在我正确读取异常之前就关闭了。我将用我当前的代码更新问题,并询问如何更好地实现队列,因为我仍然不明白。

标签: python python-3.x multithreading tkinter treeview


【解决方案1】:

我为 tkinter 线程添加了一个类,Tkworker,它使用 Queue 和 after()。要完成的工作被分成块(参见:self.chunksize=500)。打印一些东西到终端检查订单。

import random, threading, queue
from tkinter import Tk, TclError, Frame, IntVar, Scrollbar, Button
from tkinter.font import Font
from tkinter.ttk import Progressbar, Treeview
from string import printable

COLS = list(range(10))
ROWS = 1500


def __main__():
    root = Tk()
    program_window = App(root)
    try:
        root.destroy()
    except TclError:
        pass


class Tkworker:
    Empty = queue.Empty

    def __init__(self, root, producer, consumer, ms=200):
        self.root = root
        self.consumer = consumer
        self.ms = ms
        self.queue = queue.Queue()  # type: queue.Queue
        self.thread = threading.Thread(target=producer)

    def start(self):
        self.stop = False
        self.thread.start()
        self._consumer_call()

    def put(self, item):
        self.queue.put(item)

    def get(self):
        return self.queue.get(False)

    def _consumer_call(self):
        self.consumer()
        if not self.stop:
            self.root.after(self.ms, self._consumer_call)


class App(Frame):

    def __init__(self, parent=None):
        self.parent = parent

        self.bar_int = IntVar(value=0)
        self.tree = Treeview(self.parent, columns=COLS, show="headings")
        self.vsb = Scrollbar(self.parent, orient='vertical', command=self.tree.yview)
        self.bar = Progressbar(self.parent, variable=self.bar_int)
        self.btn = Button(self.parent, text="Populate", command=self.start_entries)

        for col in COLS:
            self.tree.heading(col, text=str(col))
            self.tree.column(col, width=Font().measure(str(col)))

        self.tree.grid(row=0, column=0, sticky='nsew', columnspan=2)
        self.vsb. grid(row=0, column=2, sticky='nsew')
        self.bar. grid(row=1, column=0, sticky='nsew')
        self.btn. grid(row=1, column=1, sticky='nsew', columnspan=2)

        self.parent.columnconfigure(0, weight=1)
        self.parent.rowconfigure(0, weight=1)
        self.parent.geometry('300x200')

        self.parent.mainloop()

    def start_entries(self):
        self.chunksize = 500
        self.tkwr = Tkworker(self.parent, self.worker, self.add_entries, ms=100)
        self.tree.delete(*self.tree.get_children())
        self.bar.configure(maximum=ROWS)
        self.num = 0
        self.tkwr.start()

    def worker(self):
        chunk = []
        for i in range(ROWS):
            chunk.append([random.sample(printable, 10) for _ in COLS])
            if  i % self.chunksize == 0:
                self.tkwr.put(chunk)
                chunk = []
        if chunk:
            self.tkwr.put(chunk)
        print('worker end')

    def add_entries(self):
        try:
            chunk = self.tkwr.get()
            for li in chunk:
                self.tree.insert('', 'end', values=li)
            self.num += len(chunk)
            self.bar_int.set(self.num)
        except self.tkwr.Empty:
            if self.num == ROWS:
                self.tkwr.stop = True
                print('stop')


if __name__ == "__main__":
    __main__()

【讨论】:

  • 所以我花了一些时间试图更好地理解代码,但我仍然不确定是否包含第二个类来处理前端。据我了解,你没有理由不能让 App 类自己处理它,唯一的好处是,如果你需要用 App 类中需要的另一组函数做同样的事情,它会稍微容易一些无论如何。否则对我来说,它似乎增加了过度的复杂性,额外的一层混淆了正在发生的事情。上我没有看到的课程还有其他好处吗?
  • 这个类是给我自己的,所以我可以在不改变局部或全局的情况下重用代码,你可以删除类容器,这不是强制性的。
  • 您有什么方法建议在内部或在另一个工人之前呼叫第二个工人?我目前有一个训练网络按钮和扫描按钮,如果尚未训练网络,我希望扫描自动训练网络,但我不确定如何在扫描函数中实现该检查和调用没有单个消费者函数来处理两个队列。
  • 如果要做的工作不需要更新 tkinter,你就不需要使用 tkworker。您可以使用普通线程。
  • 启动扫描线程,最后,在消费者函数中启动另一个具有另一个生产/消费者的 tworker。最好使用类 Tworker 来完成这项工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-26
  • 1970-01-01
相关资源
最近更新 更多