关闭窗口时写入tkinter IntVar致线程挂起的问题及解决
线程操作Tkinter变量导致窗口关闭时挂起的问题与解决
问题描述
使用线程执行长时间文件处理任务时,点击关闭窗口后,线程在尝试设置IntVar时挂起且无报错。由于该任务是文件处理关键模块,中途终止会引发数据问题,因此不能使用守护线程。
简化后的复现代码如下:
import tkinter as tk import tkinter.ttk as ttk import threading class Window(tk.Tk): def __init__(this, *args, **kwargs) -> None: super().__init__(*args, **kwargs) this.threads = [] this.var = tk.IntVar(value=0) this.label = ttk.Label(textvariable=this.var) this.button = ttk.Button(text='Start counter', command=this.startCounter) this.label.pack() this.button.pack() this.stop = False this.protocol("WM_DELETE_WINDOW", this.close) def startCounter(this): thread = threading.Thread(target=this.counter) this.threads.append(thread) thread.start() def counter(this): while True: if this.stop: print(f'self.stop = {this.stop}') break this.var.set(this.var.get() + 1) def close(this): print('Stopping threads') this.stop = True this.waitThreads() print('Stopped threads') this.destroy() def waitThreads(this): for thread in this.threads: thread.join() Window().mainloop()
实际场景中用IntVar绑定进度条,曾尝试多种停止线程的方法无效;仅当在停止检查前提前记录变量值时能正常停止,但测试脚本无法复现该现象。期望设置变量的代码能正常执行或报错,而非挂起。
问题原因
- Tkinter线程非安全性:Tkinter的所有GUI组件(包括
IntVar这类变量)仅允许主线程(运行mainloop的线程)操作,子线程直接调用this.var.set()属于线程不安全操作,会触发Tkinter内部锁机制。 - 死锁形成:关闭窗口时,主线程调用
waitThreads()执行thread.join()阻塞等待子线程结束;而子线程此时正尝试调用this.var.set(),该操作需要等待主线程事件循环处理GUI更新,但主线程已被join阻塞,最终双方互相等待形成死锁,导致线程挂起。
解决方法(不使用守护线程)
1. 线程安全队列传递数据,主线程更新变量
子线程仅负责计算任务,将需要更新的进度值放入线程安全队列,主线程通过after()方法定期从队列取数据并更新IntVar,避免子线程直接操作GUI变量。
2. 使用threading.Event作为停止标志
threading.Event的set()和is_set()方法是线程安全的,比普通布尔变量更可靠,能确保子线程及时感知停止信号。
3. 非阻塞式等待线程结束
不在close()方法中直接调用thread.join()阻塞主线程,而是用after()定期检查线程状态,待所有线程结束后再销毁窗口。
修改后的示例代码
import tkinter as tk import tkinter.ttk as ttk import threading import queue class Window(tk.Tk): def __init__(this, *args, **kwargs) -> None: super().__init__(*args, **kwargs) this.threads = [] this.var = tk.IntVar(value=0) # 线程安全队列用于传递进度值 this.progress_queue = queue.Queue() # 线程停止标志 this.stop_event = threading.Event() this.label = ttk.Label(textvariable=this.var) this.button = ttk.Button(text='Start counter', command=this.startCounter) this.label.pack() this.button.pack() this.protocol("WM_DELETE_WINDOW", this.close) # 启动主线程的队列检查任务 this.check_queue() def check_queue(this): """主线程定期从队列取数据更新IntVar""" try: while not this.progress_queue.empty(): value = this.progress_queue.get_nowait() this.var.set(value) except queue.Empty: pass # 未停止则继续定期检查 if not this.stop_event.is_set(): this.after(100, this.check_queue) def startCounter(this): thread = threading.Thread(target=this.counter) this.threads.append(thread) thread.start() def counter(this): count = 0 while not this.stop_event.is_set(): count += 1 # 将计算结果放入队列,由主线程更新 this.progress_queue.put(count) def close(this): print('Stopping threads') this.stop_event.set() # 非阻塞式等待线程结束 this.wait_for_threads() def wait_for_threads(this): alive_threads = [t for t in this.threads if t.is_alive()] if alive_threads: # 100ms后再次检查线程状态 this.after(100, this.wait_for_threads) else: print('Stopped threads') this.destroy() Window().mainloop()
内容的提问来源于stack exchange,提问作者ego-lay atman-bay
相关产品推荐
相关产品推荐

