You need to enable JavaScript to run this app. 关闭基于tkinter的串口数据采集应用时出现多类异常的问题咨询
优惠活动
大模型
产品
解决方案
定价
更多

关闭基于tkinter的串口数据采集应用时出现多类异常的问题咨询

报错的核心根源
  • 你使用的continuous_threading.PeriodicThread默认是非守护线程,程序全程没有在退出流程中主动终止该线程、释放资源。Python解释器退出时会等待所有非守护线程结束,但此时主线程已经销毁了Tkinter上下文、串口句柄,工作线程还卡在ser.readline()的阻塞调用上,最终导致解释器收尾阶段写stderr时拿不到全局锁,触发could not acquire lock的致命错误。
  • 你之前添加的stop退出标志存在逻辑漏洞:stop仅会在readSerial函数启动时被检查,但你初始化串口时没有设置读取超时,ser.readline()默认是永久阻塞模式,只要串口没收到带换行符的数据,线程就永远卡在readline调用上,根本走不到检查stop的分支,完全没法响应退出指令。
  • 点击窗口X后终端卡住的本质是:主线程已经调用root.destroy()关闭了Tk窗口,但工作线程还卡在串口阻塞读取上,解释器的退出流程一直在等待这个非守护线程结束,没法把终端控制权交还给用户,哪怕按Ctrl+C也没法打断这个卡住的阻塞调用。
  • 终端按Ctrl+C退出时报错的原因是:中断信号直接打断了主线程的mainloop,解释器立刻开始收尾流程,但工作线程还在运行、还持有stderr的写锁,最终触发锁竞争的致命错误。
对应可落地的解决方案
  • 给串口添加读取超时:将原串口初始化代码修改为ser = serial.Serial('COM16', baudRate, timeout=0.1),让readline最多等待0.1秒就会自动返回,从根本上避免永久阻塞,让线程能定期检查退出标志。
  • 完善窗口关闭的回调流程,依次完成资源释放:
    1. 将全局stop标志设为True,通知线程终止数据读取
    2. 主动调用ser.close()关闭串口,强制让卡在readline上的阻塞调用立刻返回
    3. 主动调用周期线程的停止方法,等待线程完全退出后再销毁Tk窗口
  • 将周期线程设置为守护线程:在创建t1 = continuous_threading.PeriodicThread(0.1, readSerial)后添加t1.daemon = True,就算出现异常没来得及手动停止线程,解释器退出时也会自动清理该线程,不会卡住终端。
  • 给主循环添加异常捕获:用try-except包裹root.mainloop()捕获KeyboardInterrupt,收到Ctrl+C中断信号时走和窗口关闭完全一致的资源释放流程,不要让解释器裸退出。
修改后的核心参考代码
import tkinter as tk
import tkinter.ttk as ttk
import serial.tools.list_ports
from tkinter import scrolledtext
import serial
import continuous_threading

HEIGHT = 700
WIDTH = 800
baudRate = 9600
stop = False

# 串口初始化加0.1秒超时,避免永久阻塞
ser = serial.Serial('COM16', baudRate, timeout=0.1)
val1 = 0

def readSerial():
    global val1, stop
    if stop:
        return
    # readline最多等0.1秒就会返回,不会卡在线程里
    ser_bytes = ser.readline()
    if ser_bytes:
        ser_bytes = ser_bytes.decode("utf-8", errors="ignore")
        val1 = ser_bytes
        scrollbar.insert("end", val1)
        scrollbar.see("end")

t1 = continuous_threading.PeriodicThread(0.1, readSerial)
# 设置为守护线程,防止退出时卡住
t1.daemon = True

def serial_ports():
    return serial.tools.list_ports.comports()

def on_select(event=None):
    global COMPort
    COMPort = cb.get()
    print(COMPort)

def on_close():
    global stop
    stop = True
    try:
        # 释放串口、停止工作线程
        ser.close()
        t1.stop()
        t1.join(timeout=0.2)
    except Exception:
        pass
    root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Sensor Interface")
    # 绑定窗口关闭回调
    root.protocol('WM_DELETE_WINDOW', on_close)

    canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
    canvas.pack()

    frame1 = tk.Frame(root)
    frame1.place(relx=0, rely=0.05, relheight=0.03, relwidth=1, anchor='nw')
    label0 = tk.Label(frame1, text="Select the COM port that the device is plugged in: ")
    label0.config(font=("TkDefaultFont", 8))
    label0.place(relx=0.1, rely=0.3, relwidth=0.3, relheight=0.5)
    cb = ttk.Combobox(frame1, values=serial_ports())
    cb.place(relx=0.5, rely=0.5, anchor='center')
    cb.bind('<<ComboboxSelected>>', on_select)

    frame2 = tk.Frame(root, bg='#80c1ff')
    frame2.place(relx=0, rely=0.1, relheight=1, relwidth=1, anchor='nw')
    scrollbar = scrolledtext.ScrolledText(frame2)
    scrollbar.place(relx=0, rely=0, relheight=1, relwidth=1, anchor='nw')

    t1.start()
    try:
        root.mainloop()
    except KeyboardInterrupt:
        # 捕获Ctrl+C,走统一退出流程
        on_close()

补充优化:子线程直接操作Tkinter组件存在线程安全隐患,如果后续遇到偶现的界面崩溃,可以把scrollbar.insert的操作通过root.after(0, scrollbar.insert, "end", val1)调度到主线程执行。


内容的提问来源于stack exchange,提问作者user1584421

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.01 12:36:07