Почему tkinter не освобождает память при уничтожении экземпляра? - PullRequest
0 голосов
/ 16 октября 2018

Я хотел бы получить быстрый и грязный способ получить некоторые имена файлов без ввода в моей оболочке, поэтому у меня есть следующий фрагмент кода:

from tkinter.filedialog import askopenfile

file = askopenfile()

Теперь все это работает нормально, но создаетлишний tkinter GUI, который нужно закрыть.Я знаю, что могу сделать это, чтобы подавить это:

import tkinter as tk
tk.Tk().withdraw()    

Но это не значит, что он не загружен сзади.Это просто означает, что теперь есть Tk() объект, который я не могу закрыть / уничтожить.


Так что это привело меня к моему настоящему вопросу.

Кажется, каждый раз, когда я создаюTk(), независимо от того, если я del или destroy(), память не освобождается.См. Ниже:

import tkinter as tk
import os, psutil
process = psutil.Process(os.getpid())
def mem(): print(f'{process.memory_info().rss:,}')

# initial memory usage
mem()

# 21,475,328
for i in range(20):
    root.append(tk.Tk())
    root[-1].destroy()
    mem()

# 24,952,832
# 26,251,264
# ...
# 47,591,424
# 48,865,280

# try deleting the root instead

del root
mem()

# 50,819,072

Как видно, python не освобождает использование даже после уничтожения каждого экземпляра Tk() и удаления root s.Однако это не относится к другим объектам:

class Foo():
    def __init__(self):
        # create a list that takes up approximately the same size as a Tk() on average
        self.lst = list(range(11500))    

for i in range(20):
    root.append(Foo())
    del root[-1]
    mem()

# 52,162,560
# 52,162,560
# ...
# 52,162,560

Поэтому мой вопрос: почему он отличается от Tk() и моего Foo(), и почему не уничтожает / удаляет * 1027?* освободили память?

Есть что-то очевидное, что я пропустил?Является ли мой тест недостаточным для подтверждения моего подозрения?Я искал здесь и в Google, но нашел мало ответов.

Редактировать: Ниже приведены несколько других методов, которые я попробовал (и не удалось) с рекомендациями в комментариях:

# Force garbage collection
import gc
gc.collect()

# quit() method
root.quit()

# delete the entire tkinter reference
del tk

Ответы [ 2 ]

0 голосов
/ 16 октября 2018

Когда вы создаете экземпляр Tk, вы создаете больше, чем просто виджет.Вы создаете объект, имеющий несколько атрибутов (встроенный интерпретатор tcl, список виджетов и т. Д.).Когда вы делаете root.destroy(), вы уничтожаете только некоторые данные, принадлежащие этому объекту.Сам объект все еще существует и занимает память.Поскольку вы сохраняете ссылку на этот объект в списке, этот объект никогда не собирается сборщиком мусора, поэтому память остается в памяти.

Когда вы создаете корневое окно с root = tk.Tk(), вы возвращаете объект (root).Если вы посмотрите на атрибуты этого объекта с помощью переменных, вы увидите следующее:

>>> root = tk.Tk()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': ['tkerror', 'exit', '4463962184destroy'], 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}

Когда вы вызываете root.destroy(), вы уничтожаете только сам виджет (по существу, элементы в _tclCommandsсписок).Другие части объекта остаются нетронутыми.

>>> root.destroy()
>>> vars(root)
{'children': {}, '_tkloaded': 1, 'master': None, '_tclCommands': None, 'tk': <_tkinter.tkapp object at 0x10a1d7f30>}

Обратите внимание, что _tclCommands был установлен на None, но остальные атрибуты все еще занимают память.Один из них, tk, занимает достаточное количество памяти, которую никогда не вернуть.

Чтобы полностью удалить объект, вам нужно удалить его.В вашем случае вам нужно удалить элемент из списка, чтобы больше не было ссылок на объект.Затем вы можете подождать, пока сборщик мусора сработает, это волшебство, или вы можете явно вызвать сборщик мусора.

Это может не вернуть 100% памяти, но должно подвести вас довольно близко.


С учетом всего сказанного, tkinter не был предназначен для такого использования.Основное ожидание состоит в том, что вы создаете один экземпляр Tk в начале вашей программы и сохраняете этот единственный экземпляр живым, пока ваша программа не завершится.

В вашем случае я рекомендую создать корневое окно один раз взапустите программу и скройте ее.Затем вы можете звонить askopenfile() так часто, как вам нравится во время вашей программы.Если вы хотите что-то более общего назначения, создайте функцию, которая создает корневое окно при первом вызове и кэширует окно, так что его нужно создать только один раз.

0 голосов
/ 16 октября 2018

Здесь есть три проблемы, одна из которых - ошибка tkinter, одна из которых ваша, а другая ведет себя как задумано.

Три проблемы:

  1. tkinter создает необнаружимый опорный цикл как часть регистрации своих обработчиков очистки, который прерывается только явным вызовом destroy (если вы не сделаете этого, опорный цикл будет никогда очищены, и ресурсы сохраняются навсегда)
  2. Вы держите свои Tk объекты даже после того, как вы destroy их
  3. Куча маленьких объектов редко, если вообще когда-либо,возвращается в ОС до завершения программы (память сохраняется для будущих распределений)

Проблема № 1 означает, что вы должны destroy любой Tk, который вы создаете явно, если таместь вероятность восстановления памяти.

Проблема № 2 означает, что вы должны явно избавиться от любой ссылки на Tk (после destroy его использования), прежде чем создавать новую, если вам нужна памятьбыть доступным для других Пуrposes.В некоторых случаях вы также можете явно установить tk.NoDefaultRoot(), чтобы предотвратить кэширование первых Tk, которые вы создаете, на tkinter в качестве корневого каталога по умолчанию (при этом явные вызовы destroy для такого объекта будуточистите кешированный корневой каталог по умолчанию, так что во многих случаях это не будет проблемой).

Проблема № 3 означает, что вы должны избавиться от ссылок с нетерпением, а не ждать до конца программы, чтобыудалите root list;если вы подождете до конца, чтобы удалить его, да, память будет возвращена в кучу, но не в ОС, поэтому будет выглядеть, как будто вы все еще используете ее.Это не настоящая проблема, хотя;неиспользуемая память будет выгружена на диск, если операционная система нуждается в оперативной памяти (обычно она распределяет пустые страницы перед активными), и ее сохранение повышает производительность большей части кода.

В частности, похоже, что атрибут .tk экземпляров Tk не очищается, даже если вы явно destroy Tk instancde .Вы можете ограничить рост памяти, изменив цикл, чтобы избавиться от последней ссылки на объект Tk, или, если вы просто хотите освободить ресурсы низкого уровня C, явно отсоедините .tk после destroy, используя новый Tk element **:

# Not necessary, but avoids caching any Tk as a root when you don't want it
tk.NoDefaultRoot()  

root = []  # Missing in your original code, but I'm assuming it was a plain list
for i in range(20):
    root.append(tk.Tk())
    root[-1].destroy()

    # Either drop the reference to the `Tk` completely:
    root[-1] = None
    # or just drop the reference to its C level worker object
    root[-1].tk = None

    # Optionally, call gc.collect() here to forcibly reclaim memory faster
    # otherwise you're likely to see memory usage grow by a few KB as uncleaned
    # cycles aren't reclaimed in time so we see phantom leaks (that would
    # eventually be cleaned)
    mem()

Явная очистка ссылки позволяет очищать базовые ресурсы на основе вывода моего слегка измененного сценария:

12,152,832
17,539,072
17,924,096  # At this point, the original code was above 18.8M bytes
17,965,056
17,965,056  # At this point, the original code was above 21.7M bytes
... remains unchanged until end of program if gc.collect() called regularly ...

Тот факт, чтопамять никогда не восстанавливается полностью для первого объекта, это не удивительно.Распределители памяти редко удосуживаются фактически вернуть память операционной системе, если только выделение не было огромным (достаточно большим, чтобы вызвать переключение режима, которое делает независимый запрос к ОС для памяти, которая управляется отдельно от "маленькой"куча объектов ").В противном случае они сохраняют свободный список памяти, который больше не используется и может быть использован повторно.

~ 6 МБ «мусора» здесь, вероятно, были кучей небольших выделений, связанных с созданием объекта TkСамо по себе и дерево объектов, которым оно управляет, которое, хотя впоследствии и возвращается в кучу для повторного использования, не будет возвращено ОС до тех пор, пока программа не выйдет (при этом, если эта часть кучи больше никогда не будет использоваться, ОС может преимущественновыгрузите неиспользуемые части на диск, если на диске не хватает памяти).Вы можете увидеть, как эта оптимизация помогла, заметив, что использование памяти стабилизируется почти сразу;новые объекты tk.Tk() просто повторно используют ту же память, что и первая (отсутствие полной стабильности, вероятно, связано с фрагментацией кучи, что приводит к необходимости небольших дополнительных выделений).

...