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

Python Threading+Tkinter:绑定窗口关闭时Event.set()无法终止线程

问题根源分析

你的代码存在三个核心问题,直接导致窗口关闭时线程无法正常终止:

  1. 子线程直接操作Tkinter GUI组件
    Tkinter是线程不安全的,所有涉及GUI的更新(比如self.canvas.draw())必须在主线程执行。你的子线程follow中直接调用update_histogram执行绘图操作,会导致GUI线程和工作线程的资源竞争,引发死锁或冻结。

  2. kill方法中的死循环阻塞主线程
    kill里的while not all_clear循环会一直占用主线程,导致Tkinter无法处理线程终止的后续事件,工作线程也无法完成最后的收尾(比如设置self.stopped),形成死锁。

  3. 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()
关键修改说明
  1. GUI操作移到主线程

    • 子线程follow只负责生成数据并存入队列,不再调用绘图方法
    • 新增process_queue方法,通过root.after()在主线程循环处理队列,完成直方图更新,避免线程安全问题
  2. 修复线程状态标记逻辑

    • 初始self.stopped = True表示线程未运行,启动时设为False,线程结束时设为True
    • 将工作线程设为守护线程(daemon=True),确保主线程退出时线程能强制终止(作为兜底)
  3. 替换阻塞式循环为非阻塞检查

    • kill方法中用root.after()循环检查线程状态,不再阻塞主线程,让Tkinter能正常处理事件循环,等待线程完成终止
  4. 优化队列处理

    • 清空队列时确保所有任务都标记为完成,避免资源泄漏

内容的提问来源于stack exchange,提问作者9iovaferra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 03:50:03