如何在包含Tkinter对象的场景下实现方法并行执行?
如何让调用Tkinter对象的两个方法并发执行?
问题场景
当指定时间过后,方法A会终止方法B。在此之前,方法A会在Tkinter标签中显示剩余时间。但目前两个方法无法并发运行。
环境信息
- Python版本:2.7
- 操作系统:Windows 7
我尝试用Threads实现并发,但了解到Python的Global Interpreter Lock(GIL)会让线程串行运行,推测这是问题根源。也试过用Processes,但Tkinter对象无法被序列化(pickle),报错:PicklingError: Can't pickle 'tkapp' object.
下方是可运行的示例(采用MVC模式),模拟实际程序:
from Tkinter import * from time import sleep from threading import Thread, Timer class Frontend(Tk): def __init__(self): Tk.__init__(self) self.label = Label(self, text = "", font = ("Courier", 12)) self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12)) self.label.grid() self.button.grid(sticky = "nsew") class Backend: def background_task(self): print "Background task is executing." sleep(6) print "Finished." class Controller: def __init__(self): self.INTERRUPT_AFTER = 4 self.done = True self.backend = Backend() self.frontend = Frontend() self.frontend.button.configure(command = self.run_something_in_background) class Decorator(object): def __init__(self, instance, time): self.instance = instance self.time = time def exit_after(self): def outer(fn): def inner(): timer = Timer(self.time, self.quit_function) timer.start() fn() return timer return inner return outer def quit_function(self): if not self.instance.done: self.instance.display_message("Interrupted background task.") self.instance.set_done(True) def run_something_in_background(self): backendThread = Thread(target = self.backend.background_task) decorator = self.Decorator(self, self.INTERRUPT_AFTER) countdown = decorator.exit_after()(self.countdown) # exit_after returns the function with which we decorate. self.set_done(False) countdown() backendThread.start() backendThread.join() self.set_done(True) def countdown(self): seconds = self.INTERRUPT_AFTER while seconds > 0 and not self.done: message = "Interrupting background task in {} seconds\nif not finished.".format(str(seconds)) self.display_message(message) seconds -= 1 sleep(1) def set_done(self, val): self.done = val def display_message(self, message): self.frontend.label.config(text = message) self.frontend.update_idletasks() def run(self): self.frontend.mainloop() app = Controller() app.run()
解决方案
其实你的问题和GIL关系不大——GIL主要限制CPU密集型任务的并行,但你的任务是IO密集型(sleep和UI更新),线程完全可以正常并发。真正的问题是你在run_something_in_background里调用了backendThread.join(),这会直接阻塞主线程,直到后台任务跑完,导致倒计时逻辑根本没法和后台任务并行;另外,你的倒计时是在主线程里执行的,会卡住Tkinter的事件循环,让UI没法响应。
修改后的代码
我们把倒计时放到独立线程,去掉阻塞主线程的join(),同时保证Tkinter的UI更新线程安全:
from Tkinter import * from time import sleep from threading import Thread, Timer class Frontend(Tk): def __init__(self): Tk.__init__(self) self.label = Label(self, text = "", font = ("Courier", 12)) self.button = Button(self, text = "Run thread in background.", font = ("Courier", 12)) self.label.grid() self.button.grid(sticky = "nsew") class Backend: def background_task(self): print "Background task is executing." sleep(6) print "Finished." class Controller: def __init__(self): self.INTERRUPT_AFTER = 4 self.done = True self.backend = Backend() self.frontend = Frontend() self.frontend.button.configure(command = self.run_something_in_background) self.interrupt_timer = None def quit_function(self): if not self.done: self.display_message("Interrupted background task.") self.set_done(True) def run_something_in_background(self): if not self.done: return # 防止用户重复点击启动多个任务 self.set_done(False) # 启动后台任务线程,设为守护线程,主线程退出时自动结束 backendThread = Thread(target=self._background_task_wrapper) backendThread.daemon = True backendThread.start() # 启动倒计时线程 countdown_thread = Thread(target=self.countdown) countdown_thread.daemon = True countdown_thread.start() # 设置中断定时器 self.interrupt_timer = Timer(self.INTERRUPT_AFTER, self.quit_function) self.interrupt_timer.start() def _background_task_wrapper(self): self.backend.background_task() # 如果任务完成时还没被中断,更新状态和提示 if not self.done: self.set_done(True) self.display_message("Background task completed successfully.") # 任务完成后取消定时器,避免误触发中断提示 if self.interrupt_timer: self.interrupt_timer.cancel() def countdown(self): seconds = self.INTERRUPT_AFTER while seconds > 0 and not self.done: message = "Interrupting background task in {} seconds\nif not finished.".format(str(seconds)) self.display_message(message) seconds -= 1 sleep(1) def set_done(self, val): self.done = val def display_message(self, message): # Tkinter不是线程安全的,所有UI操作必须在主线程执行,用after方法把任务抛到主线程事件队列 self.frontend.label.after(0, lambda: self.frontend.label.config(text=message)) def run(self): self.frontend.mainloop() app = Controller() app.run()
关键修改点说明
- 移除
join()阻塞:join()会让主线程等待后台任务完成,直接卡死UI和倒计时逻辑,我们把后台线程设为守护线程,让它在主线程退出时自动结束。 - 倒计时独立线程:把倒计时逻辑放到单独线程,让它和后台任务真正并行执行,不会阻塞主线程的事件循环。
- 线程安全的UI更新:Tkinter不允许在子线程直接操作UI,我们用
after(0, ...)方法把UI更新任务放到主线程的事件队列里,这是Tkinter推荐的跨线程更新方式。 - 任务完成时取消定时器:如果后台任务在中断时间前完成,主动取消定时器,避免弹出错误的中断提示。
- 防止重复点击:在启动任务前检查
self.done状态,避免用户多次点击按钮启动多个并行任务。
这样修改后,后台任务和倒计时就能正常并发,UI也能实时更新剩余时间,完美解决你的问题。
内容的提问来源于stack exchange,提问作者gefdel
相关产品推荐
相关产品推荐

