Python: оператор exec и непредвиденное поведение сборщика мусора - PullRequest
7 голосов
/ 09 июня 2011

Я обнаружил проблему с exec (это произошло в системе, которая должна быть расширяемой с помощью пользовательских сценариев).Я мог бы свести саму проблему к следующему коду:

def fn():
    context = {}
    exec '''
class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
x = test()''' in context

fn()

Я ожидал, что сборщик мусора освободит память после вызова функции fn.Тем не менее, процесс Python по-прежнему потребляет дополнительные 200 МБ памяти, и я абсолютно не понимаю, что здесь происходит и как освободить выделенную память вручную.

Я подозреваю, что определение класса внутри exec не являетсяочень яркая идея, но, прежде всего, я хочу понять, что идет не так в приведенном выше примере.

Похоже, что создание экземпляра класса в другой функции решает проблему, но в чем разница?

def fn():
    context = {}
    exec '''
class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()
    ''' in context
fn()

Это моя версия интерпретатора Python:

$ python
Python 2.7 (r27:82500, Sep 16 2010, 18:02:00) 
[GCC 4.5.1 20100907 (Red Hat 4.5.1-3)] on linux2

Ответы [ 2 ]

5 голосов
/ 10 июня 2011

Причина, по которой вы видите, что она занимает 200 МБ памяти дольше, чем вы ожидаете, заключается в том, что у вас есть ссылочный цикл: context - это диктант, ссылающийся как на x, так и test. x ссылается на экземпляр test, который ссылается на test. У test есть атрибут атрибутов test.__dict__, который содержит функцию __init__ для класса. Функция __init__, в свою очередь, ссылается на глобальные переменные, с которыми она была определена - это то, что вы передали exec, context.

Python прервет эти циклы ссылок для вас (поскольку ни у кого из них нет метода __del__), но для его запуска требуется gc.collect(). gc.collect() будет запускаться автоматически при каждом выделении N (определяется gc.set_threshold()), поэтому «утечка» исчезнет в какой-то момент, но если вы хотите, чтобы она немедленно исчезла, вы можете запустить gc.collect() самостоятельно или прервать цикл задания. самостоятельно перед выходом из функции. Вы можете легко сделать последнее, вызвав context.clear() - но вы должны понимать, что это влияет на все экземпляры класса, который вы создали в нем.

0 голосов
/ 10 июня 2011

Не думаю, что проблема связана с exec - сборщик мусора просто не активируется.Если вы извлечете код exec 'в основное приложение, оба способа будут иметь то же поведение, что и exec:

class test:
    def __init__(self):
        self.buf = '1'x1024*200
x = test()

# Consumes 200MB

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()

# Memory get collected correctly

Разница между этими двумя методами заключается в том, что во втором методелокальная область видимости изменяется при вызове f1(), и я думаю, что сборщик мусора запускается, когда x выходит из области видимости, поскольку функция возвращает управление в основной сценарий.Если область действия не изменяется, то сборщик мусора ждет , пока разница между числом выделений и числом освобождений не превысит его пороговое значение (на моем компьютере пороговое значение равно 700, по умолчанию работает Python 2.7).

Мы можем немного понять, что происходит:

import sys
import gc

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
x = test()

print gc.get_count()
# Prints (168, 8, 0)

Итак, мы видим, что сборщик мусора запускается много раз, но по какой-то причине не собирает x.Если вы тестируете с другой версией:

import sys
import gc

class test:
    def __init__(self):
        self.buf = '1'*1024*1024*200
def f1(): x = test()
f1()

print gc.get_count()
# Prints (172, 8, 0)

В этом случае мы знаем, что ему действительно удалось собрать x.Таким образом, кажется, что когда x объявлен в глобальной области видимости, он сохраняет некоторую циклическую ссылку на себя, которая предотвращает его сбор.Мы всегда можем использовать del x для принудительного принудительного сбора, но, конечно, это не идеально.Если вы используете gc.get_referrers(x), мы сможем увидеть, какие объекты все еще ссылаются на *1024*, и, возможно, это даст ключ к пониманию того, как этого избежать.

Я знаю, что не зналдействительно решить проблему, но, надеюсь, это помогло вам в правильном направлении.Я запомню этот вопрос на случай, если найду что-нибудь позже.

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