web-dev-qa-db-fra.com

KeyError dans le module 'threading' après une exécution py.test réussie

J'exécute un ensemble de tests avec py.test. Ils passent. Yippie! Mais je reçois ce message:

Exception KeyError: KeyError(4427427920,) in <module 'threading' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.pyc'> ignored

Comment dois-je procéder pour retrouver la source de cela? (Je n'utilise pas directement le filetage, mais j'utilise gevent.)

68
kkurian

J'ai observé un problème similaire et j'ai décidé de voir ce qui se passait exactement - permettez-moi de décrire mes conclusions. J'espère que quelqu'un le trouvera utile.

Histoire courte

Elle est en effet liée au patch de singe du module threading. En fait, je peux facilement déclencher l'exception en important le module de threading avant les threads de patch de singe. Les 2 lignes suivantes suffisent:

import threading
import gevent.monkey; gevent.monkey.patch_thread()

Une fois exécuté, il crache le message à propos de KeyError ignoré:

(env)czajnik@autosan:~$ python test.py 
Exception KeyError: KeyError(139924387112272,) in <module 'threading' from '/usr/lib/python2.7/threading.pyc'> ignored

Si vous échangez les lignes d'importation, le problème a disparu.

Longue histoire

Je pouvais arrêter mon débogage ici, mais j'ai décidé qu'il valait la peine de comprendre la cause exacte du problème.

La première étape a été de trouver le code qui imprime le message sur l'exception ignorée. C'était un peu difficile pour moi de le trouver (chercher pour Exception.*ignored N'a rien donné), mais en fouillant autour du code source CPython, j'ai finalement trouvé une fonction appelée void PyErr_WriteUnraisable(PyObject *obj) dans Python /error.c , avec un commentaire très intéressant:

/* Call when an exception has occurred but there is no way for Python
   to handle it.  Examples: exception in __del__ or during GC. */

J'ai décidé de vérifier qui l'appelle, avec un peu d'aide de gdb, juste pour obtenir la trace de pile de niveau C suivante:

#0  0x0000000000542c40 in PyErr_WriteUnraisable ()
#1  0x00000000004af2d3 in Py_Finalize ()
#2  0x00000000004aa72e in Py_Main ()
#3  0x00007ffff68e576d in __libc_start_main (main=0x41b980 <main>, argc=2,
    ubp_av=0x7fffffffe5f8, init=<optimized out>, fini=<optimized out>, 
    rtld_fini=<optimized out>, stack_end=0x7fffffffe5e8) at libc-start.c:226
#4  0x000000000041b9b1 in _start ()

Maintenant, nous pouvons clairement voir que l'exception est levée pendant que Py_Finalize s'exécute - cet appel est responsable de l'arrêt de l'interpréteur Python, de la libération de la mémoire allouée, etc. avant de sortir.

L'étape suivante consistait à regarder le code Py_Finalize() (il est dans Python/pythonrun.c ). Le tout premier appel qu'il fait est wait_for_thread_shutdown() - mérite d'être examiné, car nous savons que le problème est lié au threading. Cette fonction appelle à son tour _shutdown Appelable dans le module threading. Bon, nous pouvons revenir au code python maintenant.

En regardant threading.py, J'ai trouvé les parties intéressantes suivantes:

class _MainThread(Thread):

    def _exitfunc(self):
        self._Thread__stop()
        t = _pickSomeNonDaemonThread()
        if t:
            if __debug__:
                self._note("%s: waiting for other threads", self)
        while t:
            t.join()
            t = _pickSomeNonDaemonThread()
        if __debug__:
            self._note("%s: exiting", self)
        self._Thread__delete()

# Create the main thread object,
# and make it available for the interpreter
# (Py_Main) as threading._shutdown.

_shutdown = _MainThread()._exitfunc

De toute évidence, la responsabilité de l'appel de threading._shutdown() est de joindre tous les threads non démons et de supprimer le thread principal (quoi que cela signifie exactement). J'ai décidé de patcher un peu threading.py - envelopper le corps entier de _exitfunc() avec try/except et imprimer la trace de la pile avec traceback module. Cela a donné la trace suivante:

Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 785, in _exitfunc
    self._Thread__delete()
  File "/usr/lib/python2.7/threading.py", line 639, in __delete
    del _active[_get_ident()]
KeyError: 26805584

Nous connaissons maintenant l'endroit exact où l'exception est levée - à l'intérieur de la méthode Thread.__delete().

Le reste de l'histoire est évident après avoir lu threading.py Pendant un certain temps. Le dictionnaire _active Mappe les ID de threads (tels que renvoyés par _get_ident()) aux instances de Thread, pour tous les threads créés. Lorsque le module threading est chargé, une instance de la classe _MainThread Est toujours créée et ajoutée à _active (Même si aucun autre thread n'est explicitement créé).

Le problème est que l'une des méthodes corrigées par le patch de singe de gevent est _get_ident() - l'original est mappé sur thread.get_ident(), le patch de singe le remplace par green_thread.get_ident(). Évidemment, les deux appels renvoient des ID différents pour le thread principal.

Maintenant, si le module threading est chargé avant le patch de singe, l'appel _get_ident() renvoie une valeur lorsque l'instance _MainThread Est créée et ajoutée à _active, Et une autre valeur au moment où _exitfunc() est appelée - d'où KeyError dans del _active[_get_ident()].

Au contraire, si le patch de singe est effectué avant le chargement de threading, tout va bien - au moment où l'instance _MainThread Est ajoutée à _active, _get_ident() est déjà corrigé et le même ID de thread est renvoyé au moment du nettoyage. C'est ça!

Pour m'assurer d'importer des modules dans le bon ordre, j'ai ajouté l'extrait de code suivant à mon code, juste avant l'appel de correction de singe:

import sys
if 'threading' in sys.modules:
        raise Exception('threading module loaded before patching!')
import gevent.monkey; gevent.monkey.patch_thread()

J'espère que vous trouverez mon histoire de débogage utile :)

213
Code Painters

Vous pouvez utiliser ceci:

import sys
if 'threading' in sys.modules:
    del sys.modules['threading']
import gevent
import gevent.socket
import gevent.monkey
gevent.monkey.patch_all()
19
user2719944

J'ai eu un problème similaire avec un script prototype gevent.

Le rappel Greenlet s'exécutait correctement et je me synchronisais sur le thread principal via g.join (). Pour mon problème, j'ai dû appeler gevent.shutdown () pour arrêter (ce que je suppose être) le Hub. Après avoir arrêté manuellement la boucle d'événements, le programme se termine correctement sans cette erreur.

1
Kris