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

Tkinter报错RuntimeError:主线程不在主循环的原因与解决方法

问题:Tkinter串口实时绘图触发RuntimeError: main thread is not in main loop

我写了一个从串口读取数据并实时绘图的Tkinter程序,只做数据读取时正常,但加上实时绘图逻辑后,就触发RuntimeError: main thread is not in main loop错误。我知道要用after方法,但用的时候出了一堆bug。想知道这个错误的成因、解决方法,以及正确的after用法。

原代码:

from tkinter import * 
from random import randint 
import numpy as np
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure 
import time 
import threading
import serial
from tkinter.filedialog import asksaveasfile
import csv
from skimage.restoration import (denoise_tv_chambolle, denoise_bilateral, denoise_wavelet, estimate_sigma)

continuePlotting = False 
 
def change_state(): 
    global continuePlotting
    global serial_port
    global i
    global b_start
    global mid
    global label
    if continuePlotting == True: 
        continuePlotting = False
        serial_port.close()
        i = 2
        label.pack_forget()
    else: 
        continuePlotting = True
        label.pack(in_=mid)

t, p1 = [], []
i = 0

def take_measure():
    global t
    global p1
    global t_temp
    global p1_temp
    global i
    global serial_port
    if i == 0:
        serial_port =serial.Serial('COM5', 2000000)
        serial_port.setDTR(False)
        time.sleep(0.1)
        serial_port.setDTR(True)
        serial_port.flushInput()
        p1_temp = []
        t_temp = []
        i = 1
    elif i == 2:
        serial_port =serial.Serial('COM5', 2000000)
        serial_port.setDTR(False)
        time.sleep(0.1)
        serial_port.setDTR(True)
        serial_port.flushInput()
        p1_temp = []
        t_temp = []
        i = 1
    try:
        temp=serial_port.readline()[:-2].decode()
        a = temp.index(";")
        t_val = float(temp[:a])
        p1_val = (float(temp[a+1:])-2640)*20/3520
        t.append(t_val)
        p1.append(p1_val)
        t_temp.append(t_val)
        p1_temp.append(p1_val)
    except:
        pass

def app():
    global t_temp
    global p1_temp
    global i
    global serial_port
    global t
    global p1
    global b_start
    global mid
    global label

root = Tk() 
root.config(background='white') 
root.geometry("1000x700")

top = Frame(root)
bottom = Frame(root)
mid = Frame(root)
top.pack(side="top")
mid.pack(side="top")
bottom.pack(side="bottom", fill="both", expand=True) 
 
fig = Figure() 
 
ax = fig.add_subplot(111) 
ax.set_xlabel("X axis") 
ax.set_ylabel("Y axis") 
ax.grid()

graph = FigureCanvasTkAgg(fig, master=root) 
graph.get_tk_widget().pack(in_=bottom, side="top",fill='both',expand=True)
graph.draw()

toolbar = NavigationToolbar2Tk(graph, root, pack_toolbar=False)
toolbar.update()

def plotter():
    while continuePlotting:
        take_measure()
        ax.clear()
        ax.plot(t_temp, p1_temp)
        graph.draw()

        

def gui_handler(): 
    change_state() 
    t=threading.Thread(target=plotter, daemon=True)
    t.start()

def Save():
    files = [('CSV', '*.csv')]
    file_name = asksaveasfile(filetypes = files, defaultextension = files)
    with open(str(file_name.name),"w", newline='') as file:
        Writer=csv.writer(file)
        Writer.writerow(["temps en ms", "pression en V"]) 
        for elt in range(len(t)):
            Writer.writerow([t[elt], p1[elt]])
    file.close()

def Clear():
    global t
    global p1
    serial_port.close()
    i = 0
    ax.clear()
    graph.draw()
    t = []
    p1 = []

def Draw():
    l_t = [t[0]]
    l_p1 = [p1[0]]
    ax.cla() 
    ax.grid()
    for elt in range(1,len(t)):
        if t[elt] == 0:
            l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True')
            print("max", max(l_p1))
            print("min", min(l_p1))
            ax.plot(l_t, l_p)
            l_t, l_p1 = [], []
        l_t.append(t[elt])
        l_p1.append(p1[elt])
    l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True')
    ax.plot(l_t, l_p)
    graph.draw()
    print("max", max(l_p1))
    print("min", min(l_p1))

b_start = Button(root, text="Start/Stop", command=gui_handler, bg="red", fg="white") 
b_start.pack(in_=top, side = LEFT) 

button_quit = Button(master=root, text="Quit", command=root.destroy)
button_save = Button(root,text="Save", command=Save)
button_clear = Button(root,text="Clear graph", command=Clear)
button_draw = Button(root,text="Show graphs", command=Draw)

button_draw.pack(in_=top, side = LEFT)
button_clear.pack(in_=top, side = LEFT)
button_save.pack(in_=top, side = LEFT)
button_quit.pack(in_=top, side=LEFT)

label = Label(root, text = "WAIT")

toolbar.pack(in_=bottom, fill=X)

root.mainloop() 


if __name__ == '__main__': 
    app() 

报错信息:

Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0>
Traceback (most recent call last):
  File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__
    if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: main thread is not in main loop
Exception ignored in: <function Image.__del__ at 0x000001CCB1098D60>
Traceback (most recent call last):
  File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 4083, in __del__
    self.tk.call('image', 'delete', self.name)
RuntimeError: main thread is not in main loop
Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0>
Traceback (most recent call last):
  File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__
    if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: main thread is not in main loop
Exception ignored in: <function Variable.__del__ at 0x000001CCB0F3E3E0>
Traceback (most recent call last):
  File "c:\Users\cbroggi\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 410, in __del__
    if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: main thread is not in main loop

错误成因

  • Tkinter是单线程GUI库,所有GUI相关操作(绘图、控件更新、Canvas绘制等)必须在主线程(启动root.mainloop()的线程)中执行,子线程不能直接操作GUI元素。
  • 你的代码里,plotter函数在子线程中直接调用ax.clear()、graph.draw()这些GUI操作,违反了Tkinter的线程安全规则,导致主线程的GUI循环和子线程操作冲突,触发错误。
  • 那些被忽略的异常,是子线程结束时尝试访问已被主线程销毁的Tkinter资源导致的。

解决方法

核心思路:子线程只负责读取串口数据,主线程负责更新GUI,用线程安全的队列传递数据,通过after方法让主线程定期更新绘图:

  1. 用queue.Queue在子线程和主线程之间传递读取到的串口数据,避免全局变量的线程冲突。
  2. 子线程仅执行串口数据读取,将数据放入队列,不碰任何GUI代码。
  3. 主线程通过root.after(delay_ms, func)定期调用更新绘图的函数,从队列中取出数据并更新图表,完全符合Tkinter的线程规则。

修改后的完整代码

from tkinter import * 
import numpy as np
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure 
import threading
import serial
import time
from tkinter.filedialog import asksaveasfile
import csv
from skimage.restoration import denoise_wavelet
import queue

# 全局控制开关和数据队列
continuePlotting = False 
data_queue = queue.Queue()
serial_port = None
t, p1 = [], []
t_temp, p1_temp = [], []

def change_state(): 
    global continuePlotting
    global serial_port
    global label
    if continuePlotting: 
        continuePlotting = False
        if serial_port and serial_port.is_open:
            serial_port.close()
        label.pack_forget()
    else: 
        continuePlotting = True
        label.pack(in_=mid)
        # 启动串口读取子线程
        threading.Thread(target=serial_reader, daemon=True).start()
        # 启动主线程绘图更新
        update_plot()

def serial_reader():
    """子线程:仅负责读取串口数据,放入队列"""
    global serial_port, t, p1, t_temp, p1_temp
    try:
        serial_port = serial.Serial('COM5', 2000000)
        serial_port.setDTR(False)
        time.sleep(0.1)
        serial_port.setDTR(True)
        serial_port.flushInput()
        # 重置临时数据列表
        t_temp.clear()
        p1_temp.clear()
    except Exception as e:
        print(f"串口打开失败: {e}")
        continuePlotting = False
        return

    while continuePlotting and serial_port.is_open:
        try:
            temp = serial_port.readline()[:-2].decode().strip()
            if not temp:
                continue
            a = temp.index(";")
            t_val = float(temp[:a])
            p1_val = (float(temp[a+1:])-2640)*20/3520
            # 更新全局数据和临时绘图数据
            t.append(t_val)
            p1.append(p1_val)
            t_temp.append(t_val)
            p1_temp.append(p1_val)
            # 把新数据放入队列(这里也可以只传新值,按需调整)
            data_queue.put((t_val, p1_val))
        except Exception as e:
            # 捕获异常避免子线程崩溃
            print(f"读取数据出错: {e}")
            continue
    # 线程结束时关闭串口
    if serial_port.is_open:
        serial_port.close()

def update_plot():
    """主线程:定期更新绘图,用after循环调用"""
    global t_temp, p1_temp
    if continuePlotting:
        # 清空画布并重新绘图
        ax.clear()
        ax.set_xlabel("X axis")
        ax.set_ylabel("Y axis")
        ax.grid()
        ax.plot(t_temp, p1_temp)
        graph.draw()
        # 每隔50ms再次调用自己,实现循环更新
        root.after(50, update_plot)
    else:
        # 停止时可以做一些清理
        pass

def Save():
    global t, p1
    files = [('CSV', '*.csv')]
    file_name = asksaveasfile(filetypes=files, defaultextension=files)
    if not file_name:
        return
    with open(file_name.name, "w", newline='') as file:
        Writer = csv.writer(file)
        Writer.writerow(["temps en ms", "pression en V"]) 
        for t_val, p_val in zip(t, p1):
            Writer.writerow([t_val, p_val])

def Clear():
    global t, p1, t_temp, p1_temp, serial_port, continuePlotting
    continuePlotting = False
    if serial_port and serial_port.is_open:
        serial_port.close()
    # 清空所有数据
    t.clear()
    p1.clear()
    t_temp.clear()
    p1_temp.clear()
    # 清空画布
    ax.clear()
    ax.grid()
    graph.draw()

def Draw():
    global t, p1
    if not t or not p1:
        return
    ax.cla() 
    ax.grid()
    l_t = [t[0]]
    l_p1 = [p1[0]]
    for elt in range(1, len(t)):
        if t[elt] == 0:
            l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True')
            print("max", max(l_p1))
            print("min", min(l_p1))
            ax.plot(l_t, l_p)
            l_t, l_p1 = [], []
        l_t.append(t[elt])
        l_p1.append(p1[elt])
    # 处理最后一段数据
    l_p = denoise_wavelet(np.array(l_p1), method="VisuShrink", mode="hard", wavelet_levels=3, wavelet='haar', rescale_sigma='True')
    ax.plot(l_t, l_p)
    graph.draw()
    print("max", max(l_p1))
    print("min", min(l_p1))

# 初始化GUI
root = Tk() 
root.config(background='white') 
root.geometry("1000x700")

top = Frame(root)
bottom = Frame(root)
mid = Frame(root)
top.pack(side="top")
mid.pack(side="top")
bottom.pack(side="bottom", fill="both", expand=True) 

fig = Figure() 
ax = fig.add_subplot(111) 
ax.set_xlabel("X axis")
ax.set_ylabel("Y axis")
ax.grid()

graph = FigureCanvasTkAgg(fig, master=root) 
graph.get_tk_widget().pack(in_=bottom, side="top", fill='both', expand=True)
graph.draw()

toolbar = NavigationToolbar2Tk(graph, root, pack_toolbar=False)
toolbar.update()

# 按钮和标签
b_start = Button(root, text="Start/Stop", command=change_state, bg="red", fg="white") 
b_start.pack(in_=top, side=LEFT) 

button_draw = Button(root, text="Show graphs", command=Draw)
button_clear = Button(root, text="Clear graph", command=Clear)
button_save = Button(root, text="Save", command=Save)
button_quit = Button(master=root, text="Quit", command=root.destroy)

button_draw.pack(in_=top, side=LEFT)
button_clear.pack(in_=top, side=LEFT)
button_save.pack(in_=top, side=LEFT)
button_quit.pack(in_=top, side=LEFT)

label = Label(root, text="WAIT")

toolbar.pack(in_=bottom, fill=X)

root.mainloop() 

关键说明

  1. 线程分工明确:子线程serial_reader只做串口读取,主线程update_plot只做GUI更新,彻底避免线程冲突。
  2. after方法的正确用法:root.after(50, update_plot)让主线程每隔50ms自动调用update_plot,形成循环更新,完全在Tkinter的主循环中执行,不会触发线程错误。
  3. 线程安全数据传递:
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 00:11:05