2017-06-27 49 views
2

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

下面的代码是我已经能够想出的最小例子。请注意,我没有使用matplotlib做任何实际工作,但我无法触发问题。 Widget上的__del__方法验证Widget实例正在从另一个线程中删除。典型的输出是:

Running off thread on 140653207140096 
Being deleted... <__main__.Widget object .!widget2>118576 
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异常。但是我可以看到在非主线程上调用了析构函数。

回答

0

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

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

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

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

希望这将让你在正确的方向。

+0

恐怕我已经知道这一点。问题是'__del__'是垃圾收集系统调用的一种特殊方法。在我给出的最小例子中,我不会从错误的线程中调用任何'Tkinter'代码:而是垃圾收集系统,它似乎超出了我的控制范围。 –

1

我有完全相同的问题

这是要找到问题的原因是一场噩梦。我极力证实,任何线程都不会调用tkinter对象。我创建了一个基于队列的机制来处理线程中的tkinter对象。 网上有很多关于如何做这件事的例子,或者...搜索模块'mttkinter',Tkinter的线程安全包装器)

为了强制垃圾回收,我使用了“gc “方法在我的应用程序的每个TopLevel窗口的退出功能。

#garbage collector 
import gc 

... 

gc.collect() 

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

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

PutInQueue(gc.collect) 

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

希望这份报告能够对某人有很大的实用性,或者它是这种情况,揭露垃圾收集器中的任何最终错误。