关闭基于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秒就会自动返回,从根本上避免永久阻塞,让线程能定期检查退出标志。 - 完善窗口关闭的回调流程,依次完成资源释放:
- 将全局
stop标志设为True,通知线程终止数据读取 - 主动调用
ser.close()关闭串口,强制让卡在readline上的阻塞调用立刻返回 - 主动调用周期线程的停止方法,等待线程完全退出后再销毁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
相关产品推荐
相关产品推荐

