【问题标题】:Tkinter objects being garbage collected from the wrong threadTkinter 对象从错误的线程被垃圾收集
【发布时间】:2017-11-30 14:20:18
【问题描述】:

我似乎通过使用一些多线程来破坏 Linux 上的tkinter。据我所见,我设法在不是主 GUI 线程的线程上触发垃圾收集。这导致__del__tk.StringVar 实例上运行,该实例试图从错误的线程调用tcl 堆栈,从而导致linux 混乱。

下面的代码是我能想到的最小示例。请注意,我没有对matplotlib 进行任何实际工作,但否则我无法触发问题。 Widget 上的 __del__ 方法验证是否正在从另一个线程中删除 Widget 实例。典型的输出是:

Running off thread on 140653207140096
Being deleted... <__main__.Widget object .!widget2> 140653210118576
Thread is 140653207140096
... (omitted stack from from `matplotlib`
  File "/nfs/see-fs-02_users/matmdpd/anaconda3/lib/python3.6/site-packages/matplotlib/text.py", line 218, in __init__
    elif is_string_like(fontproperties):
  File "/nfs/see-fs-02_users/matmdpd/anaconda3/lib/python3.6/site-packages/matplotlib/cbook.py", line 693, in is_string_like
    obj + ''
  File "tk_threading.py", line 27, in __del__
    traceback.print_stack()
...
Exception ignored in: <bound method Variable.__del__ of <tkinter.StringVar object at 0x7fec60a02ac8>>
Traceback (most recent call last):
  File "/nfs/see-fs-02_users/matmdpd/anaconda3/lib/python3.6/tkinter/__init__.py", line 335, in __del__
    if self._tk.getboolean(self._tk.call("info", "exists", self._name)):
_tkinter.TclError: out of stack space (infinite loop?)

通过修改tkinter 库代码,我可以验证__del__ 是从与Widget.__del__ 相同的位置调用的。

我的结论正确吗?我怎样才能阻止这种情况发生??

我真的真的很想从一个单独的线程调用matplotlib 代码,因为我需要生成一些渲染速度很慢的复杂绘图,所以让它们脱离线程,生成图像,然后在tk.Canvas 小部件似乎是一个优雅的解决方案。

小例子:

import tkinter as tk
import traceback
import threading

import matplotlib
matplotlib.use('Agg')
import matplotlib.figure as figure
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas

class Widget(tk.Frame):
    def __init__(self, parent):
        super().__init__(parent)
        self.var = tk.StringVar()
        #tk.Entry(self, textvariable=self.var).grid()
        self._thing = tk.Frame(self)
        def task():
            print("Running off thread on", threading.get_ident())
            fig = figure.Figure(figsize=(5,5))
            FigureCanvas(fig)
            fig.add_subplot(1,1,1)
            print("All done off thread...")
        #import gc
        #gc.collect()
        threading.Thread(target=task).start()

    def __del__(self):
        print("Being deleted...", self.__repr__(), id(self))
        print("Thread is", threading.get_ident())
        traceback.print_stack()

root = tk.Tk()
frame = Widget(root)
frame.grid(row=1, column=0)

def click():
    global frame
    frame.destroy()
    frame = Widget(root)
    frame.grid(row=1, column=0)

tk.Button(root, text="Click me", command=click).grid(row=0, column=0)

root.mainloop()

请注意,在示例中,我不需要 tk.Entry 小部件。 但是如果我注释掉self._thing = tk.Frame(self) 行,那么我不能重现问题!这个我没看懂……

如果我取消注释然后 gc 行,那么问题也会消失(这符合我的结论......)

更新: 这似乎在 Windows 上的工作方式相同。 Windows 上的tkinter 似乎更能容忍在“错误”线程上被调用,所以我没有得到_tkinter.TclError 异常。但我可以看到在非主线程上调用了__del__ 析构函数。

【问题讨论】:

    标签: python-3.x matplotlib tkinter python-multithreading


    【解决方案1】:

    我也遇到了同样的问题

    找到问题的原因是一场噩梦。我彻底验证了没有从任何线程调用 tkinter 对象。我创建了一种基于队列的机制来处理线程中的 tkinter 对象。 网上有很多关于如何做到这一点的示例,或者......搜索模块“mttkinter”,Tkinter 的线程安全包装器)

    为了强制垃圾回收,我在我的 App 的每个 TopLevel 窗口的退出函数中使用了“gc”方法。

    #garbage collector
    import gc
    
    ...
    
    gc.collect()
    

    但由于某种原因,关闭顶层窗口继续重现问题。无论如何...正是在前面提到的“mttkinter”模块中使用了一些打印,我检测到,尽管小部件是在主线程中创建的,但当在另一个线程内触发垃圾收集器时,它们可能会被垃圾收集。看起来垃圾收集器收集了所有垃圾,没有任何区别(主线程或其他线程?)。如果我错了,请有人纠正我。

    我的解决方案是也使用队列显式调用垃圾收集器。

    PutInQueue(gc.collect)
    

    “PutInQueue”属于我创建的一个模块,用于处理 tkinter 对象和其他类型的具有线程安全性的对象。

    希望这份报告对某人有很大的帮助,或者,它可以揭露垃圾收集器中的任何最终错误。

    【讨论】:

    • 这是救命稻草!我花了两个小时试图在我的代码中找到一个错误,但在阅读了这篇文章之后,我了解到它实际上是 Python 垃圾收集器在 Tkinter 小部件上的从属线程上调用 __del__,这些小部件前一段时间在 MainThread 被销毁。解决方案是在 MainThread 上销毁 tk.Tk 之后但在从线程上调用 start() 之前添加 gc.collect()
    【解决方案2】:

    Tkinter 不是线程安全的。在线程中调用 Tkinter 对象可能会导致诸如“Widget 上的 del 方法验证 Widget 实例是否正在从另一个线程中删除。”

    您可以使用锁定和队列来使其正确完成。

    检查这个例子: Tkinter: How to use threads to preventing main event loop from "freezing"

    还有这个例子(你可以找到很多其他例子): Mutli-threading python with Tkinter

    希望这会让你朝着正确的方向前进。

    【讨论】:

    • 恐怕我已经知道了。问题在于__del__ 是一种特殊方法,由垃圾回收系统调用。在我给出的最小示例中,我没有从错误的线程调用任何Tkinter 代码:而是垃圾收集系统调用了,这似乎超出了我的控制范围......
    猜你喜欢
    • 2011-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-06
    • 1970-01-01
    • 2020-05-08
    • 1970-01-01
    相关资源
    最近更新 更多