пятница, 21 ноября 2008 г.

Управление памятью в Питоне

Питон - язык с динамической типизацией и встроенным менеджером управления памятью. Присутствует еще длинный ряд особенностей и достоинств, но сейчас мы поговорим не о них, а именно о memory management.

На первый взгляд все просто: программист создает объект, а когда этот объект становится не нужен - он автоматически удаляется. Обычно так все и происходит, но иногда система дает сбой - память постоянно растет, встроенный garbage collector не работает.
Программист впадает в легкую панику и начинает ругать сквозь зубы "чертов язык программирования" и самого Гвидо ван Россума, попутно стараясь разобраться в проблеме. Чаще всего это заканчивается откатом исходников до версии "все вроде бы хорошо" - без понимания настоящей причины.

На самом деле такой подход никуда не годится... На те же грабли можно наступить опять ненароком, и снова потратить значительное время на решение. Да и вообще как-то не неловко - убежал, вместо того, чтобы осознать и победить.

Я попытаюсь показать, как работает этот механизм управления памятью в Питоне, и, главное, как он не работает. Чудеса - только в сказках.

Итак, с созданием объекта проблем никогда не бывает - только с его удалением. К слову, в Питоне все - объекты. Классы, переменные, функции, модули, число 3.14 и строка 'я - Андрей'. Исключений нет.

Проблемы бывают с удалением. Вернее, само по себе освобождение памяти тоже работает отлично. Беда в другом. Программист ожидает, что объект будет удален - ведь он больше этому программисту не нужен. А Питон думает иначе. В результате - конфликт интересов, Питон априори выигрывает. Программист получает головную боль.

Чтобы понять, почему эта ситуация иногда происходит, нужно углубиться в детали.

Исторически сложилось так, что в Питоне существуют два механизма для освобождения памяти - decref и garbage collector. Считаю это большим достоинством, и чуть позже объясню почему.

Начнем с первого. Питон считает ссылки на объект. Это количество переменных, ссылающихся на него.

>>> import sys
>>> a = 'Hello world'
>>> b = a
>>> sys.getrefcount(a)
3
>>> sys.getrefcount(b)
3
>>> del a
>>> sys.getrefcount(b)
2

Всегда отнимаем единицу от getrefcount - она автоматически добавляется при вызове функции.

Пока что все просто. Когда удаляется ссылка - счетчик уменьшается на единицу. Когда он становится равнным нулю - удаляется сам объект. Это - decref (по названию макроса в C API, делающего всю работу).

Теперь - более сложный пример. Дерево.

>>> class Parent(object):
...     def __init__(self):
...         self.children = []
...     def add(self, ch):
...         self.children.append(ch)
...         ch.parent = self
...
>>> class Child(object):
...     def __init__(self):
...         self.parent = None
...
>>>
>>> p = Parent()
>>> p.add(Child())
>>> p
<__main__.parent>
>>> p.children
[<__main__.child>]
>>> sys.getrefcount(p)
3
>>> sys.getrefcount(p.children[0])
3

Parent имеет ссылку на child, а тот в свою очередь - на родителя. Даже если мы удалим все внешние ссылки - они друг на друга все еще ссылаются, счетчик ссылок у каждого по единице (если добавили несколько child - у parent, соответствено, больше). Объекты остались в памяти, хотя программисту они уже не нужны - он "выбросил их и забыл". Проблема, думаю, ясна. Мусор.

Тогда появился второй способ - garbage collector (кажется, начиная с версии 2.1. Точно не помню, но помню, как ему радовался). Управление им - через модуль gc.
Коротко работу собирателя мусора можно описать так:
- есть три поколения объектов
- когда новый объект создается - сразу же попадает в первое поколение
- считается количество созданных/удаленных объектов
- если разница больше порога - запускается умный cycle finder
- если объект все еще не удален даже gc - он перемещается в более старое поколение
- если дело совсем худо - поймаем нашего нарушителя в gc.garbage
- все настраивается - смотрите документацию по gc.
- gc предоставляет еще много интересной информации, как-то: кто ссылается на объект и на кого он ссылается, кто попадает в garbage, список всех объектов, живущих в Питоне и т.д.
- детали на самом деле не очень важны.

Cycle finder пытается найти cycle dependencies - циклические зависимости (я буду называть их кольцами) - и удалить их. Т.е. если ты ссылаешься на меня, а я на тебя - и никто на нас снаружи - мы попадем под garbage collector и нас успешно разименуют.
Рано или поздно. Пиковое потребление памяти может быть довольно большим, но "в среднем по больнице температура 36.6". В тяжелых случаях можно принудительно запустить gc.collect() - но это выглядит как-то не кошерно.

Обычно все работает и позволяет ничего не делать в случае parent-child. Проблемы возникают, когда один из объектов кольца имеет метод __del__ или написан как extension, т.е. не на Питоне. Второй случай замнем для ясности - хотя для меня он весьма актуален.

Вернемся к __del__. Очень полезный метод, позволяющий сделать "уборку за собой". Например, закрыть файл логов, отсоединиться от базы данных и т.д. Проблема в том, что garbage collector вычислил кольцо, в котором, возможно, есть несколько объектов с __del__. И __del__ от parent может использовать свой child, который уже удален - получим странную ошибку. В такие интимные детали garbage collector не вникает, просто помещает все кольцо в мусорник - gc.garbage.
При этом оставляя программисту возможность посмотреть на это безобразие и разрулить ситуацию самому. Никогда такого не делал и считаю дурным тоном - мало ли кто в мусорник попадет, а мне за всех отвечать...

Для решения сложных проблем с кольцами быстро появился еще один стандартный модуль - weakref. Т.е. слабая ссылка, которая как бы видит другой объект, но при этом не увеличивает его счетчик.

Достоинства двойного способа удаления объектов:
- если вы аккуратны и внимательны - получите минимальное использование памяти и явные вызовы __del__ aka destructor.
- иначе обычно объект все же удалится, пусть не сразу и с некторыми ограничениями. Таких примеров - большинсво.

Если будет интересно - о слабых ссылках в следующей статье.