【发布时间】: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