2011-06-09 9 views
6

he encontrado un problema con exec (Sucedió en un sistema que tiene que ser extensible con guiones escritos por el usuario). Podría reducir el problema en sí a este código:Python: sentencia exec y el comportamiento recolector de basura inesperada

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

fn() 

que se espera que la memoria debe ser liberado por el recolector de basura después de la llamada de la función fn. Sin embargo, el proceso de Python aún consume los 200 MB adicionales de memoria y no tengo ni idea de qué está sucediendo aquí y cómo liberar la memoria asignada manualmente.

Sospecho que la definición de una clase dentro de exec no es una idea muy brillante, pero, antes que nada, quiero entender qué está fallando en el ejemplo anterior.

Parece que la creación de instancias de clases de envoltura en otra función se soluciona el problema, pero ¿cuál es la diferencia?

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

Esta es mi versión intérprete de 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 
+0

¿Hacer lo mismo en su código (sin pasar por una cadena y 'exec') da los mismos resultados? – delnan

+3

'gc.collect()' parece resolverlo. Debe haber un ciclo cíclico en alguna parte. Adivinando salvajemente, x tiene una referencia a su clase, la clase probablemente tiene una referencia al espacio de nombre en el que está definida, y que a su vez tiene una referencia de regreso a x. –

+0

Lo mismo ocurre en mi código sin que el administrador funcione bien, y el recolector de basura funciona como se esperaba. – 3xter

Respuesta

5

La razón de que se está viendo se tarda hasta 200 MB de memoria durante más tiempo de lo esperado es porque tiene un ciclo de referencia: context es una dict que hace referencia tanto a x como a test. x hace referencia a una instancia de test, que hace referencia a test. test tiene un dict de atributos, test.__dict__, que contiene la función __init__ para la clase. La función __init__ a su vez hace referencia a las variables globales que se definió con - que es el dict que pasó a exec, context.

Python romperá estos ciclos de referencia por usted (ya que nada involucrado tiene un método __del__) pero requiere gc.collect() para ejecutarse. gc.collect() se ejecutará automáticamente cada N asignaciones (determinado por gc.set_threshold()) por lo que la "fuga" va a desaparecer en algún momento, pero si quieres que se vaya inmediatamente puede ejecutar gc.collect() usted mismo, o romper el ciclo de referencia a sí mismo antes de salir de la función. Puede hacer esto último fácilmente llamando al context.clear(), pero debe tener en cuenta que eso afecta a todas las instancias de la clase que creó en él.

0

No creo que el problema tiene que ver con exec - el recolector de basura no se está activando. Si extrae el código exec 'd a cabo con la aplicación principal, ambas formas dan el mismo comportamiento que con exec:

class test: 
    def __init__(self): 
     self.buf = '1'*1024*1024*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 

Las diferencias entre los dos métodos es que, en el segundo, el ámbito local cambia cuando Se llama a f1(), y creo que el recolector de elementos no utilizados se activa cuando x queda fuera del alcance, ya que la función devuelve el control al script principal. Si el alcance no cambia, entonces el recolector de basura espera until the difference between the number of allocations and the number of deallocations exceeds its threshold (en mi máquina, el umbral es 700 por defecto, ejecutando Python 2.7).

Podemos entender un poco de lo que está pasando:

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) 

Por lo tanto, vemos que el recolector de basura incendios hasta en numerosas ocasiones, pero por alguna razón no recoge x.Si prueba con la otra versión:

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) 

En este caso, sabemos que se las arregla para recoger x. Por lo tanto, parece que cuando se declara x en el ámbito global, conserva alguna referencia cíclica a sí misma que impide que se recopile. Siempre podemos usar del x para forzar manualmente la colección, pero por supuesto eso no es ideal. Si usa gc.get_referrers(x), podremos ver qué objetos aún se refieren a x, y tal vez eso le dará una pista sobre cómo evitar que esto suceda.

Sé que realmente no resolvió el problema, pero espero que esto te haya ayudado en la dirección correcta. Tendré presente este problema, en caso de que encuentre algo más tarde.

+0

El recolector de basura cíclico no se "dispara" para destruir las variables locales; no está involucrado en las variables locales como tal. Python utiliza el recuento de referencias, y la destrucción de variables locales es tan simple como una operación de decref. El recopilador del módulo 'gc' es algo separado, y realmente solo se dispara cuando se alcanza el umbral de asignación (o cuando lo llamas manualmente, por supuesto). –

+0

@Thomas: Ah, ya veo. Llamar 'gc.collect()' manualmente no destruye 'x' si no se evalúa dentro de' exec', sin embargo. ¿Por qué sería eso? – voithos

+0

No estoy seguro del caso que está describiendo aquí. El colector de gc cíclico solo recopilará * ciclos de referencia coleccionables * inalcanzables. Referencias a objetos 'x' es una variable local, es un nombre, no un objeto. Tiene una referencia. La cosa 'x' * se refiere a * puede ser inalcanzable, pero solo por' x' ya no existe. O la cosa 'x' se refiere a que puede ser parte de un ciclo de referencia inalcanzable, pero solo si el marco que contiene' x' también es parte de ese ciclo de referencia inalcanzable. –

Cuestiones relacionadas