终止线程时tkinter界面为何出现阻塞?
Tkinter线程终止时GUI挂起问题分析与解决
问题描述
尝试创建工作线程管理其他线程/进程并与Tkinter GUI通信,GUI继承自tkinter.Frame,包含启动按钮、进度条和可变标签。点击启动按钮后工作线程独立运行,通过event_generate()和Queue回传进度。正常运行和线程自然结束无问题,但调用threading.Event设置终止信号并join等待线程退出时,GUI会挂起,直到join超时才恢复。预期设置终止信号后工作线程立即触发<<MyEventAborted>>并退出,join应立即结束,实际却未达预期(Python 3.9.12)。
复现代码
import queue import threading import time import tkinter as tk from tkinter import ttk class GUI(tk.Frame): def __init__(self, parent, *args, **kwargs): tk.Frame.__init__(self, parent, *args, **kwargs) self.parent = parent self.init_elements() def init_elements(self): self.button = ttk.Button(self, text="Start", command=self.start_worker) self.button.grid(row=0, column=0, sticky="W", padx=5, pady=5) self.label = tk.StringVar(value="nothing yet") ttk.Label(self, textvariable=self.label).grid(row=0, column=1, sticky="E", padx=5, pady=5) self.progress = tk.DoubleVar(0) ttk.Progressbar( self, orient=tk.HORIZONTAL, length=200, mode='determinate', maximum=100, variable=self.progress ).grid(row=1, column=0, columnspan=2, sticky="WE", padx=5, pady=5) self.bind("<<MyEventUpdate>>", self.on_update) self.bind("<<MyEventFinished>>", self.on_finished) self.bind("<<MyEventAborted>>", self.on_aborted) self.com_queue = queue.Queue() def on_update(self, event): progress = self.progress.get() self.progress.set(progress+1) self.label.set(f"Task {(self.com_queue.get()):.0f}/100") def on_finished(self, event): self.label.set("Finished!") self.button.configure(text="Start", command=self.start_worker) def on_aborted(self, event): self.label.set("Aborted!") self.button.configure(text="Start", command=self.start_worker) def start_worker(self): self.progress.set(0) self.button.configure(text="Stop", command=self.stop_worker) self.worker_thread = MyStoppableThread(queue=self.com_queue, tk_gui=self) self.worker_thread.start() def stop_worker(self): self.worker_thread.stop() self.worker_thread.join(timeout=3) self.button.configure(text="Start", command=self.start_worker) class MyStoppableThread(threading.Thread): def __init__(self, queue, tk_gui, *args, **kwargs): super().__init__(*args, **kwargs) self.queue = queue self.tk_gui = tk_gui self._stop_event = threading.Event() def stop(self): self._stop_event.set() def stopped(self): return self._stop_event.is_set() def run(self): task_num = 0 while task_num < 100: if self.stopped(): self.tk_gui.event_generate("<<MyEventAborted>>", when="now") print("Stopped") return time.sleep(0.1) task_num += 1 self.queue.put(task_num) self.tk_gui.event_generate("<<MyEventUpdate>>", when="tail") self.tk_gui.event_generate("<<MyEventFinished>>", when="tail") if __name__ == "__main__": # initialize GUI and tkinter root = tk.Tk() GUI(root).grid(column=0, row=0, sticky="NSWE") # Start GUI root.mainloop()
问题原因
核心问题出在stop_worker方法的join调用:
- Tkinter的事件循环是单线程的,
join会让GUI线程进入阻塞状态,无法处理任何事件。 - 工作线程检测到终止信号后调用
event_generate("<<MyEventAborted>>", when="now"),但该操作需要GUI线程处理事件队列才能完成,而GUI线程此时卡在join上,导致工作线程的事件发送操作被阻塞,无法及时退出,最终join只能等待超时。
解决方法
避免在GUI线程中调用join阻塞事件循环,让工作线程通过事件通知GUI线程完成后续状态更新。
修改后的代码
import queue import threading import time import tkinter as tk from tkinter import ttk class GUI(tk.Frame): def __init__(self, parent, *args, **kwargs): tk.Frame.__init__(self, parent, *args, **kwargs) self.parent = parent self.worker_thread = None # 显式初始化线程变量 self.init_elements() def init_elements(self): self.button = ttk.Button(self, text="Start", command=self.start_worker) self.button.grid(row=0, column=0, sticky="W", padx=5, pady=5) self.label = tk.StringVar(value="nothing yet") ttk.Label(self, textvariable=self.label).grid(row=0, column=1, sticky="E", padx=5, pady=5) self.progress = tk.DoubleVar(0) ttk.Progressbar( self, orient=tk.HORIZONTAL, length=200, mode='determinate', maximum=100, variable=self.progress ).grid(row=1, column=0, columnspan=2, sticky="WE", padx=5, pady=5) self.bind("<<MyEventUpdate>>", self.on_update) self.bind("<<MyEventFinished>>", self.on_finished) self.bind("<<MyEventAborted>>", self.on_aborted) self.com_queue = queue.Queue() def on_update(self, event): progress = self.progress.get() self.progress.set(progress+1) self.label.set(f"Task {(self.com_queue.get()):.0f}/100") def on_finished(self, event): self.label.set("Finished!") self.button.configure(text="Start", command=self.start_worker) self.worker_thread = None # 清空线程引用 def on_aborted(self, event): self.label.set("Aborted!") self.button.configure(text="Start", command=self.start_worker) self.worker_thread = None # 清空线程引用 def start_worker(self): self.progress.set(0) self.button.configure(text="Stop", command=self.stop_worker) self.worker_thread = MyStoppableThread(queue=self.com_queue, tk_gui=self) self.worker_thread.start() def stop_worker(self): if self.worker_thread and self.worker_thread.is_alive(): self.worker_thread.stop() # 移除join调用,不再阻塞GUI线程 self.button.configure(text="Stopping...", state="disabled") # 禁用按钮防止重复点击 class MyStoppableThread(threading.Thread): def __init__(self, queue, tk_gui, *args, **kwargs): super().__init__(*args, **kwargs) self.queue = queue self.tk_gui = tk_gui self._stop_event = threading.Event() def stop(self): self._stop_event.set() def stopped(self): return self._stop_event.is_set() def run(self): task_num = 0 while task_num < 100: if self.stopped(): # 使用when="tail"让事件加入队列尾部,避免阻塞工作线程 self.tk_gui.event_generate("<<MyEventAborted>>", when="tail") print("Stopped") return time.sleep(0.1) task_num += 1 self.queue.put(task_num) self.tk_gui.event_generate("<<MyEventUpdate>>", when="tail") self.tk_gui.event_generate("<<MyEventFinished>>", when="tail") if __name__ == "__main__": # initialize GUI and tkinter root = tk.Tk() GUI(root).grid(column=0, row=0, sticky="NSWE") # Start GUI root.mainloop()
修改要点
- 移除
join调用:stop_worker不再阻塞GUI线程,让事件循环正常运行。 - 事件回调中清空线程引用:在
on_finished和on_aborted中重置线程变量,避免无效引用。 - 优化事件发送:工作线程发送终止事件时使用
when="tail",确保事件加入队列后立即返回,不阻塞线程退出。 - 添加按钮状态控制:点击Stop后禁用按钮,防止用户重复操作引发错误。
内容的提问来源于stack exchange,提问作者YPOC
相关产品推荐
相关产品推荐

