3 votes

Auto-déblocage dû au garbage collector dans un code monofilaire

J'ai un problème de conception : il y a une ressource globale à laquelle on ne peut pas accéder depuis plusieurs threads à la fois, et j'ai donc besoin d'un verrou autour d'elle pour sérialiser l'accès à cette ressource. Cependant, le ramasseur d'ordures de Python peut exécuter une commande __del__ pendant que j'effectue un traitement tout en maintenant le verrou. Si le destructeur essaie d'accéder à la ressource, cela aboutit à un blocage.

À titre d'exemple, considérons le code monofilaire d'apparence innocente suivant, qui se bloque si vous l'exécutez :

import threading

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)

    def __del__(self):
        self.close()

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)

La ressource peut gérer plusieurs "handles" ouverts en même temps, et je dois gérer leur cycle de vie. En abstrayant cela dans un Handle et de mettre le nettoyage dans __del__ semblait être un choix intelligent, mais le problème ci-dessus le brise.

Une façon de gérer le nettoyage est de garder une liste de handles "pending cleanup", et si le verrou est maintenu quand __del__ est exécuté, insérez la poignée à cet endroit, et nettoyez la liste plus tard.

La question est la suivante :

  • Existe-t-il une version threadsafe de gc.disable() / gc.enable() qui résoudrait le problème de manière plus propre ?

  • D'autres idées pour régler ce problème ?

1voto

Thomas Orozco Points 15280

Le collecteur d'ordures de Python ne nettoiera pas les dépendances circulaires qui ont un nom "personnalisé". __del__ méthode .

Puisque vous avez déjà un __del__ il suffit d'une dépendance circulaire pour "désactiver" la GC pour ces objets :

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

Maintenant, ça crée une fuite de mémoire, alors comment on répare ça ?

Lorsque vous êtes prêt à libérer les objets, il suffit de retirer la dépendance circulaire :

import threading
import gc

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)

    def __del__(self):
        self.close()

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)

# Make sure the GC is up to date
gc.collect()
print "Length after work", len(gc.garbage)

# These are kept along due to our circular depency
# If we remove them from garbage, they come back
del gc.garbage[:]
gc.collect()
print "Length now", len(gc.garbage)

# Let's break it
for handle in gc.garbage:
    handle._self = None

# Now, our objects don't come back
del gc.garbage[:]
gc.collect()
print "Length after breaking circular dependencies", len(gc.garbage)

Je le ferai :

Length after work 999
Length now 999
Length after breaking circular dependencies 0

D'autre part, pourquoi avoir besoin d'accéder à cette bibliothèque complexe dans du code de nettoyage, dont vous ne contrôlez pas l'exécution ?

Une solution plus propre ici pourrait être de faire le nettoyage dans la boucle, et de rompre la dépendance circulaire après le nettoyage, de sorte que le GC puisse ensuite faire son travail.

Voici une mise en œuvre :

import threading
import gc

class Handle(object):
    def __init__(self):
        self.handle = do_stuff("get")
        self._self = self

    def close(self):
        h = self.handle
        self.handle = None
        if h is not None:
            do_stuff("close %d" % h)
        del self._self

    def __del__(self):
        # DO NOT TOUCH THIS
        self._ = None    

_resource_lock = threading.Lock()

def do_stuff(what):
    _resource_lock.acquire()
    try:
        # GC can be invoked here -> deadlock!
        for j in range(20):
            list()
        return 1234
    finally:
        _resource_lock.release()

for j in range(1000):
    xs = []
    b = Handle()
    xs.append(b)
    xs.append(xs)

# Make sure the GC is up to date
gc.collect()
print "Length after work", len(gc.garbage)

# These are kept along due to our circular depency
# If we remove them from garbage, they come back
del gc.garbage[:]
gc.collect()
print "Length now", len(gc.garbage)

# Let's break it
for handle in gc.garbage:
    handle.close()

# Now, our objects don't come back
del gc.garbage[:]
gc.collect()
print "Length after breaking circular dependencies", len(gc.garbage)

Et la sortie montre que notre dépendance circulaire empêche la collecte :

Length after work 999
Length now 999
Length after breaking circular dependencies 0

0voto

Hu Bo Points 1

Les références circulaires ne sont pas la clé de ce problème. Vous pouvez avoir des objets a y b se référant les uns aux autres pour former un cercle, et a.resource pointer vers un objet c con __del__ . Après a y b sont collectés (ils n'ont pas __del__ Il est donc possible de les collecter en toute sécurité), c est collecté automatiquement, et c.__del__ s'appelle. Cela peut se produire partout dans le code, et vous ne pouvez pas le contrôler, ce qui peut créer un verrou mort.

Il existe également d'autres implémentations de Python (par exemple PyPy) sans comptage de références. Avec ces interpréteurs, les objets sont toujours collectés par GC.

La seule façon sûre d'utiliser __del__ utilise des opérations atomiques. Les verrous NE FONCTIONNENT PAS : soit ils se verrouillent à mort ( threading.Lock ), ou ne fonctionnent jamais ( threading.RLock ). Puisque l'ajout à une liste est une opération atomique en Python, vous pouvez mettre certains drapeaux (ou certaines fermetures) dans une liste globale, et vérifier la liste dans d'autres threads pour exécuter la "destruction réelle".

Le nouveau mode GC introduit dans Python 3.7 pourrait résoudre le problème. https://www.python.org/dev/peps/pep-0556/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X