You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 01:44:56