关闭Tkinter应用时线程无法正常结束的问题排查
问题:Tkinter计时器线程关闭异常
我用Python的Tkinter开发了一个仅含单个按钮的计时器,按钮同时显示剩余时间,计时器通过线程更新按钮文本。线程通过while循环运行,设置Event事件时循环终止。关闭窗口时用protocol方法调用函数设置Event并join线程,多数情况正常,但特定时机关闭程序时,线程会在窗口关闭后继续运行。
我不想使用thread.stop()方法,简化后的复现代码如下:
import tkinter as tk from tkinter import TclError, ttk from datetime import timedelta import time import threading from threading import Event def strfdelta(tdelta): # Includes microseconds hours, rem = divmod(tdelta.seconds, 3600) minutes, seconds = divmod(rem, 60) return str(hours).rjust(2, '0') + ":" + str(minutes).rjust(2, '0') + \ ":" + str(seconds).rjust(2, '0') + ":" + str(tdelta.microseconds).rjust(6, '0')[0:2] class App(tk.Tk): def __init__(self): super().__init__() self.is_running = False is_closing = Event() self.start_time = timedelta(seconds=4, microseconds=10, minutes=0, hours=0) self.current_start_time = self.start_time self.time_of_last_pause = time.time() self.time_of_last_unpause = None # region guisetup self.time_display = None self.geometry("320x110") self.title('Replace') self.resizable(False, False) box1 = self.create_top_box(self) box1.place(x=0, y=0) # endregion guisetup self.timer_thread = threading.Thread(target=self.timer_run_loop, args=(is_closing, )) self.timer_thread.start() def on_close(): # This occasionally fails when we try to close. is_closing.set() # This used to be a boolean property self.is_closing. Making it an event didn't help. print("on_close()") try: self.timer_thread.join(timeout=2) finally: if self.timer_thread.is_alive(): self.timer_thread.join(timeout=2) if self.timer_thread.is_alive(): print("timer thread is still alive again..") else: print("timer thread is finally finished") else: print("timer thread finished2") self.destroy() self.protocol("WM_DELETE_WINDOW", on_close) def create_top_box(self, container): box = tk.Frame(container, height=110, width=320) box_m = tk.Frame(box, bg="blue", width=320, height=110) box_m.place(x=0, y=0) self.time_display = tk.Button(box_m, text=strfdelta(self.start_time), command=self.toggle_timer_state) self.time_display.place(x=25, y=20) return box def update_shown_time(self, time_to_show: timedelta = None): print("timer_run_loop must finish. flag 0015") # If the window closes at this point, everything freezes self.time_display.configure(text=strfdelta(time_to_show)) print("timer_run_loop must finish. flag 016") def toggle_timer_state(self): # update time_of_last_unpause if it has never been set if not self.is_running and self.time_of_last_unpause is None: self.time_of_last_unpause = time.time() if self.is_running: self.pause_timer() else: self.start_timer_running() def pause_timer(self): pass # Uses self.time_of_last_unpause, Alters self.is_running, self.time_of_last_pause, self.current_start_time def timer_run_loop(self, event): while not event.is_set(): if not self.is_running: print("timer_run_loop must finish. flag 008") self.update_shown_time(self.current_start_time) print("timer_run_loop must finish. flag 018") print("timer_run_loop() ending") def start_timer_running(self): pass # Uses self.current_start_time; Alters self.is_running, self.time_of_last_unpause if __name__ == "__main__": app = App() app.mainloop()
无需点击按钮,多次按Alt+F4即可触发问题。问题出现时最后打印内容为“timer_run_loop must finish. flag 0015”,说明self.time_display.configure(text=strfdelta(time_to_show))未执行完成,推测线程调用Tkinter按钮的configure方法时关闭窗口引发了问题。
想知道是否有更合适的资源共享模式,或者更优的线程终止方式?
解决方案
1. 遵循Tkinter线程安全原则:线程不直接操作UI
Tkinter的所有GUI操作必须在主线程执行,线程只负责时间计算逻辑,通过主线程的after()方法更新UI:
- 修改线程逻辑,仅更新时间变量,不调用UI操作方法
- 在主窗口中添加定时任务,由主线程检查时间变化并更新按钮文本
修改后的timer_run_loop:
def timer_run_loop(self, event): while not event.is_set(): if not self.is_running: # 仅更新时间变量,交给主线程处理UI self.current_display_time = self.current_start_time time.sleep(0.01) # 降低循环频率,减少资源占用 print("timer_run_loop must finish. flag 018") print("timer_run_loop() ending")
在App类的__init__中添加主线程UI更新循环:
def __init__(self): # ... 原有代码 ... self.current_display_time = self.start_time self.update_ui() def update_ui(self): try: self.time_display.configure(text=strfdelta(self.current_display_time)) except TclError: # 窗口已销毁,终止定时更新 return # 每10ms触发一次UI更新 self.after(10, self.update_ui)
2. 在线程中操作UI时捕获TclError
如果必须在线程中临时操作UI,必须捕获窗口销毁时触发的TclError,避免线程因异常卡住:
修改update_shown_time方法:
def update_shown_time(self, time_to_show: timedelta = None): print("timer_run_loop must finish. flag 0015") try: self.time_display.configure(text=strfdelta(time_to_show)) except TclError: # 窗口已销毁,直接返回 pass print("timer_run_loop must finish. flag 016")
3. 优化窗口关闭逻辑,避免线程阻塞
将on_close中的join和销毁逻辑拆分,用after()让主线程先处理完当前事件,再尝试join线程:
def __init__(self): # ... 原有代码 ... self.is_closing = Event() # 把is_closing设为实例变量,方便其他方法访问 self.timer_thread = threading.Thread(target=self.timer_run_loop, args=(self.is_closing, )) self.timer_thread.start() def on_close(): self.is_closing.set() print("on_close()") # 延迟执行join和销毁,给线程处理剩余逻辑的时间 self.after(100, self.join_and_destroy) self.protocol("WM_DELETE_WINDOW", on_close) def join_and_destroy(self): self.timer_thread.join(timeout=1) if self.timer_thread.is_alive(): print("timer thread is still alive, force exiting") else: print("timer thread finished") self.destroy()
内容的提问来源于stack exchange,提问作者wimworks
相关产品推荐
相关产品推荐

