Python Threading+Tkinter:绑定窗口关闭时Event.set()无法终止线程
问题根源分析
你的代码存在三个核心问题,直接导致窗口关闭时线程无法正常终止:
子线程直接操作Tkinter GUI组件
Tkinter是线程不安全的,所有涉及GUI的更新(比如self.canvas.draw())必须在主线程执行。你的子线程follow中直接调用update_histogram执行绘图操作,会导致GUI线程和工作线程的资源竞争,引发死锁或冻结。kill方法中的死循环阻塞主线程kill里的while not all_clear循环会一直占用主线程,导致Tkinter无法处理线程终止的后续事件,工作线程也无法完成最后的收尾(比如设置self.stopped),形成死锁。stopped变量的逻辑完全混乱start方法里错误地将self.stopped设为True(应该是False,表示线程正在运行)- 线程结束时又重复设置
self.stopped = True,和运行状态的标记完全矛盾
修复后的代码
import tkinter as tk from tkinter import ttk from threading import Thread, Event from queue import Queue import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import numpy as np class Histogram(): def __init__(self, root: tk.Tk): self.root = root self.buffer: list[float] = [] self.event = Event() self.queue = Queue() self.stopped = True # 初始状态:线程未运行 self.fig, self.ax = plt.subplots(figsize=(4, 3), dpi=64, layout='tight') self.ax.set_xlim(0, 80) self.ax.set_ylim(0, 30) self.ax.set_xlabel('Time (ns)') self.ax.set_ylabel('Counts') self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) self.canvas.draw() self.canvas.get_tk_widget().grid( column=0, columnspan=2, row=1, padx=6, pady=6, sticky='nesw' ) # 启动GUI更新的循环 self.root.after(100, self.process_queue) def start(self) -> None: if not self.stopped: return # 防止重复启动线程 self.cleanup() self.event.clear() self.stopped = False self.thread = Thread(target=self.follow, daemon=True) # 设置为守护线程 self.thread.start() self.root.protocol('WM_DELETE_WINDOW', self.kill) def follow(self) -> None: count = 1 while not self.event.is_set(): data = np.random.normal(loc=40.0, scale=10.0) self.queue.put((data, count)) count += 1 # 线程结束标记 self.stopped = True def process_queue(self): # 主线程处理队列,更新GUI while not self.queue.empty(): data, count = self.queue.get() self.buffer.append(data) if count % 5 == 0: if self.ax.patches: _ = [b.remove() for b in self.ax.patches] counts, bins = np.histogram(self.buffer, bins=80, range=(0, 80)) self.ax.stairs(counts, bins, color='blueviolet', fill=True) y_upper_lim = self.ax.get_ylim()[1] if np.max(counts) > y_upper_lim * 0.95: self.ax.set_ylim(0, y_upper_lim + 10) self.canvas.draw() self.queue.task_done() # 持续检查队列 if not self.stopped: self.root.after(100, self.process_queue) def cleanup(self) -> None: if self.ax.patches: _ = [b.remove() for b in self.ax.patches] self.buffer = [] # 清空队列 while not self.queue.empty(): self.queue.get() self.queue.task_done() def stop(self) -> None: self.event.set() def kill(self) -> None: self.stop() # 等待线程终止,用after循环代替死循环,不阻塞主线程 def check_thread(): if self.thread.is_alive(): self.root.after(50, check_thread) else: print(f'{self.thread.is_alive()=}') self.root.quit() self.root.destroy() check_thread() def main(): padding = dict(padx=12, pady=12, ipadx=6, ipady=6) root = tk.Tk() root.title('Live Histogram') hist = Histogram(root=root) start_button = ttk.Button(root, text='START', command=hist.start) start_button.grid(column=0, row=0, **padding, sticky='new') stop_button = ttk.Button(root, text='STOP', command=hist.stop) stop_button.grid(column=1, row=0, **padding, sticky='new') root.mainloop() if __name__ == '__main__': main()
关键修改说明
GUI操作移到主线程
- 子线程
follow只负责生成数据并存入队列,不再调用绘图方法 - 新增
process_queue方法,通过root.after()在主线程循环处理队列,完成直方图更新,避免线程安全问题
- 子线程
修复线程状态标记逻辑
- 初始
self.stopped = True表示线程未运行,启动时设为False,线程结束时设为True - 将工作线程设为守护线程(
daemon=True),确保主线程退出时线程能强制终止(作为兜底)
- 初始
替换阻塞式循环为非阻塞检查
kill方法中用root.after()循环检查线程状态,不再阻塞主线程,让Tkinter能正常处理事件循环,等待线程完成终止
优化队列处理
- 清空队列时确保所有任务都标记为完成,避免资源泄漏
内容的提问来源于stack exchange,提问作者9iovaferra
相关产品推荐
相关产品推荐

