Python 3 Tkinter Treeview多线程插入数据性能异常缓慢问题排查与求助
Hey there, let's break down your questions and tackle this Treeview performance issue step by step!
1. Explanation of the Tkinter methods causing bottlenecks
Let's unpack those two methods from your profiling results:
'{method 'call' of '_tkinter.tkapp' objects}': This is the low-level method Python uses to send commands to the underlying Tcl interpreter that powers Tkinter. Every widget operation—likeTreeview.insert—gets routed through this method. When you call these operations from a background thread, Tkinter's thread-safety constraints kick in: Tcl is single-threaded, so every cross-thread call requires synchronization (like locks) and context switching. This overhead piles up massively with 1500+ calls, which is why it eats up most of your runtime.'{method 'globalsetvar' of '_tkinter.tkapp' objects}': This method handles updates to Tcl global variables. In your code,self.bar_int.set(i)triggers this call becauseIntVaris bound to a Tcl global variable behind the scenes. Cross-thread updates to these variables add synchronization overhead, and each update also forces the Progressbar to redraw—adding even more unnecessary work while populating the Treeview.
2. Does Tkinter Treeview support a fixed-height model like GTK3?
Unfortunately, the ttk Treeview doesn't have a built-in fixed-height model option like GTK3's. However, you can replicate a similar effect by temporarily hiding the Treeview while inserting data. As you noticed, when the Treeview is not visible, it skips rendering each row immediately, which cuts down runtime drastically. This is the closest workaround to a fixed-height model in Tkinter.
3. Stable, efficient optimization solutions
Here are a few proven approaches to fix your performance issue:
Option 1: Batch insert in the main thread (with temporary hiding)
Since Tkinter works best when widget operations are done in the main thread, this is the simplest and most stable fix. Hide the Treeview during insertion to skip per-row rendering, then show it once all data is added:
def add_entries(self): # Hide Treeview to disable rendering during insertion self.tree.grid_remove() self.tree.delete(*self.tree.get_children()) self.bar.configure(maximum=ROWS) # Pre-generate all data first (optional, but keeps logic clean) data = [] for i in range(ROWS): self.bar_int.set(i) # Fix: Convert random.sample result to a string (Treeview expects string values) row = [''.join(random.sample(printable, 10)) for _ in COLS] data.append(row) # Insert all rows for row in data: self.tree.insert('', 'end', values=row) # Show Treeview again to render all rows at once self.tree.grid()
Option 2: Optimize thread-to-main-thread communication
If you need to keep data generation in a background thread, use a thread-safe queue and batch processing in the main thread to reduce overhead:
- Replace your list-based queue with
queue.Queue(thread-safe and efficient) - Process batches of rows instead of one at a time to minimize
after()calls - Adjust the
after()interval to balance UI responsiveness and insertion speed
4. Evaluation of your current after() + Queue implementation
Your current approach is on the right track, but it has a few key issues:
- Unsafe list-based queue: Using a regular list with
pop(0)is not thread-safe—you could hit race conditions between your background thread adding data and the main thread removing it. Plus,pop(0)is an O(n) operation, which gets slow with 1500+ items. - Single-item processing: Handling one row per
refreshercall wastes time on repeatedafter()scheduling. Processing batches (e.g., 50 rows at a time) is much more efficient. - No cleanup for completed threads: Your code doesn't handle the case where the background thread finishes but the queue is empty—you'll keep scheduling
refresherunnecessarily.
Improved implementation
Here's a refined version using queue.Queue and batch processing:
import queue # Add this import class App(Frame): def __init__(self, parent=None): # ... existing __init__ code ... self.data_queue = queue.Queue() # Replace list with thread-safe queue def refresher(self): processed = 0 BATCH_SIZE = 50 # Adjust based on your UI needs # Process a batch of rows while processed < BATCH_SIZE and not self.data_queue.empty(): i, item = self.data_queue.get() self.bar_int.set(i) self.tree.insert('', 'end', values=item) processed += 1 # Check if we need to keep refreshing if self.btn['state'] == 'DISABLED': if not self.data_queue.empty(): # Schedule next refresh with a short interval for responsiveness self.parent.after(10, self.refresher) else: # Worker is done, re-enable button self.btn.configure(state=NORMAL) def add_entries(self): self.btn.configure(state=DISABLED) self.tree.delete(*self.tree.get_children()) self.bar.configure(maximum=ROWS) self.parent.after(10, self.refresher) # Start the refresh loop worker = threading.Thread(target=self.add_entries_worker) worker.start() def add_entries_worker(self): try: for i in range(ROWS): row = [''.join(random.sample(printable, 10)) for _ in COLS] self.data_queue.put((i, row)) finally: # Ensure any remaining cleanup happens (optional) pass
This version is thread-safe, more efficient, and properly handles the completion of the background thread.
内容的提问来源于stack exchange,提问作者MufinMcFlufin




