TK相机应用中线程调用ImageTk.PhotoImage()引发退出死锁问题
Tkinter相机应用退出时线程死锁问题
我正在开发一款Tkinter相机应用,需要从相机缓冲区获取图像并实时显示在Tk Label组件中,核心显示线程代码如下:
def display_thread_run(self): while self.is_acquiring: image_data = self.camera.get_next_image() # --- 省略部分图像转换逻辑 --- pil_image = Image.fromarray(image_data) photo = ImageTk.PhotoImage(image=pil_image) self.image_frame.after(0, self._create_photo_and_update_gui, photo) time.sleep(0.01)
应用退出时,我需要按顺序执行:关闭相机流、等待线程结束、最终释放相机资源,清理代码如下:
def stop_and_cleanup(self): camera.stop_acquisition() self.is_acquiring = False if self.display_thread and self.display_thread.is_alive(): self.display_thread.join() camera.release() logger.info("Display resources released")
但点击窗口关闭按钮后,photo = ImageTk.PhotoImage(image=pil_image)会引发死锁,线程无法被join。设置thread.daemon=True并移除display_thread.join()的方案不可行,因为必须等线程完全清理完成后才能调用camera.release()。
使用环境:
- Python 3.10.11
- Pillow 11.2.1
最小复现代码:
import tkinter as tk from PIL import Image, ImageTk import threading import time IMG_PATH = 'test_img.jpg' class App: def __init__(self, root): self.root = root self.label = tk.Label(root) self.label.pack() self.running = True self.img = Image.open(IMG_PATH) self.display_thread= threading.Thread(target=self.update_image_loop, daemon=True) self.display_thread.start() self.root.protocol("WM_DELETE_WINDOW", self.on_close) def update_image_loop(self): while self.running: tk_img = ImageTk.PhotoImage(image=self.img) self.label.after(0, self.set_image, tk_img) time.sleep(0.001) def set_image(self, tk_img): self.label.img = tk_img # 保留引用防止被GC回收 self.label.config(image=tk_img) def on_close(self): print("on_close") self.running = False self.display_thread.join() self.root.destroy() if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop()
问题原因
死锁的核心是:ImageTk.PhotoImage的创建会隐式调用Tkinter的GUI线程资源(依赖底层Tcl解释器)。关闭流程中,on_close在GUI线程调用display_thread.join()阻塞了GUI线程;此时后台线程若刚好在创建PhotoImage,会等待GUI线程的Tcl资源,形成互相等待的死锁。
解决方法
1. 把PhotoImage创建移到GUI线程
后台线程只负责获取/处理图像数据,通过after将数据传给GUI线程,由GUI线程完成PhotoImage创建和Label更新,避免后台线程操作Tk资源:
def display_thread_run(self): while self.is_acquiring: image_data = self.camera.get_next_image() # 保留图像转换逻辑 pil_image = Image.fromarray(image_data) # 传递PIL图像给GUI线程处理 self.image_frame.after(0, self._update_gui_with_image, pil_image) time.sleep(0.01) def _update_gui_with_image(self, pil_image): # 在GUI线程中创建PhotoImage并更新 photo = ImageTk.PhotoImage(image=pil_image) self.image_frame.img = photo # 保留引用 self.image_frame.config(image=photo)
2. 优化关闭流程,避免GUI线程阻塞
不在on_close中直接调用join,而是用after定期检查线程状态,线程结束后再执行清理:
def on_close(self): print("on_close") self.running = False camera.stop_acquisition() # 轮询线程状态,不阻塞GUI线程 self.root.after(50, self._wait_for_thread_and_cleanup) def _wait_for_thread_and_cleanup(self): if self.display_thread.is_alive(): self.root.after(50, self._wait_for_thread_and_cleanup) else: camera.release() logger.info("Display resources released") self.root.destroy()
3. 给线程循环增加可中断检查点
用threading.Event.wait()替代time.sleep(),可以立即唤醒线程,加快关闭速度:
def __init__(self, root): # 初始化中断事件 self.stop_event = threading.Event() # ... 其他初始化代码 ... def display_thread_run(self): while self.is_acquiring: image_data = self.camera.get_next_image() pil_image = Image.fromarray(image_data) self.image_frame.after(0, self._update_gui_with_image, pil_image) # 可中断等待,收到信号立即退出 if self.stop_event.wait(0.01): break def on_close(self): self.is_acquiring = False self.stop_event.set() # 唤醒线程 # ... 后续清理逻辑 ...
最小复现代码修复版本
import tkinter as tk from PIL import Image, ImageTk import threading import time IMG_PATH = 'test_img.jpg' class App: def __init__(self, root): self.root = root self.label = tk.Label(root) self.label.pack() self.running = True self.stop_event = threading.Event() self.img = Image.open(IMG_PATH) self.display_thread = threading.Thread(target=self.update_image_loop) self.display_thread.start() self.root.protocol("WM_DELETE_WINDOW", self.on_close) def update_image_loop(self): while self.running: # 后台线程仅传递原始PIL图像 self.label.after(0, self._create_photo_and_update, self.img) # 可中断等待 if self.stop_event.wait(0.001): break def _create_photo_and_update(self, pil_img): # GUI线程内创建PhotoImage tk_img = ImageTk.PhotoImage(image=pil_img) self.label.img = tk_img self.label.config(image=tk_img) def on_close(self): print("on_close") self.running = False self.stop_event.set() # 轮询等待线程结束 self.root.after(20, self._check_thread_and_cleanup) def _check_thread_and_cleanup(self): if self.display_thread.is_alive(): self.root.after(20, self._check_thread_and_cleanup) else: self.root.destroy() if __name__ == '__main__': root = tk.Tk() app = App(root) root.mainloop()
内容的提问来源于stack exchange,提问作者Anthony
相关产品推荐
相关产品推荐

