KeyError im Modul 'Threading' nach einem erfolgreichen py.test-Lauf

68

Ich führe eine Reihe von Tests mit py.test durch. Sie gehen vorbei. Yippie! Aber ich bekomme diese Nachricht:

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

Wie soll ich die Quelle aufspüren? (Ich verwende Threading nicht direkt, sondern gevent.)

kkurian
quelle

Antworten:

217

Ich beobachtete ein ähnliches Problem und beschloss, genau zu sehen, was los ist - lassen Sie mich meine Ergebnisse beschreiben. Ich hoffe, jemand wird es nützlich finden.

Kurzgeschichte

Es hängt in der Tat mit dem Affen-Patching des threadingModuls zusammen. Tatsächlich kann ich die Ausnahme leicht auslösen, indem ich das Threading-Modul vor dem Affen-Patching von Threads importiere. Die folgenden 2 Zeilen reichen aus:

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

Wenn es ausgeführt wird, spuckt es die Nachricht über ignoriert KeyError:

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

Wenn Sie die Importzeilen austauschen, ist das Problem behoben.

Lange Geschichte

Ich könnte mein Debuggen hier beenden, aber ich entschied, dass es sich lohnt, die genaue Ursache des Problems zu verstehen.

Der erste Schritt bestand darin, den Code zu finden, der die Nachricht über ignorierte Ausnahmen druckt. Es war ein wenig hart für mich (greppen zu finden Exception.*ignoredergab nichts), aber greppen um CPython Quellcode habe ich fand schließlich eine Funktion namens void PyErr_WriteUnraisable(PyObject *obj)in Python / error.c , mit einem sehr interessanten Kommentar:

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

Ich habe mich entschlossen, mit ein wenig Hilfe von zu überprüfen, wer es anruft, gdbum den folgenden C-Level-Stack-Trace zu erhalten:

#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 ()

Jetzt können wir deutlich sehen, dass die Ausnahme ausgelöst wird, während Py_Finalize ausgeführt wird. Dieser Aufruf ist dafür verantwortlich, den Python-Interpreter herunterzufahren, den zugewiesenen Speicher freizugeben usw. Er wird kurz vor dem Beenden aufgerufen.

Der nächste Schritt war das Betrachten von Py_Finalize()Code (in Python / pythonrun.c ). Der allererste Aufruf ist wait_for_thread_shutdown()- einen Blick wert, da wir wissen, dass das Problem mit dem Einfädeln zusammenhängt. Diese Funktion ruft wiederum _shutdownim threadingModul aufrufbare auf . Gut, wir können jetzt zum Python-Code zurückkehren.

Beim Betrachten threading.pyhabe ich folgende interessante Teile gefunden:

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

Es ist klar, dass die Verantwortung für den threading._shutdown()Aufruf darin besteht, alle Nicht-Daemon-Threads zu verbinden und den Haupt-Thread zu löschen (was auch immer das genau bedeutet). Ich habe beschlossen, threading.pyein bisschen zu patchen - den ganzen _exitfunc()Körper mit try/ zu umwickeln exceptund den Stack-Trace mit dem Traceback- Modul zu drucken . Dies ergab die folgende Spur:

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

Jetzt kennen wir den genauen Ort, an dem die Ausnahme ausgelöst wird - inside- Thread.__delete()Methode.

Der Rest der Geschichte ist nach threading.pyeiner Weile des Lesens offensichtlich . Das _activeWörterbuch ordnet Thread-IDs (wie von zurückgegeben _get_ident()) ThreadInstanzen für alle erstellten Threads zu. Beim threadingLaden des Moduls wird _MainThreadimmer eine Klasseninstanz erstellt und hinzugefügt _active(auch wenn keine anderen Threads explizit erstellt werden).

Das Problem ist, dass eine der Methoden, die durch geventdas Affen-Patchen gepatcht werden , ist _get_ident()- das Original, dem man zugeordnet ist, thread.get_ident()wird durch das Affen-Patchen ersetzt green_thread.get_ident(). Offensichtlich geben beide Aufrufe unterschiedliche IDs für den Hauptthread zurück.

Wenn nun das threadingModul vor dem Affen-Patching geladen wird, gibt _get_ident()call beim _MainThreadErstellen und Hinzufügen einer Instanz einen Wert zurück _active, und es wird jeweils ein anderer Wert _exitfunc()aufgerufen - daher KeyErrorin del _active[_get_ident()].

Im Gegenteil, wenn Affen Patching zuvor getan wird threadinggeladen wird, ist alles in Ordnung ist - im Zeitpunkt _MainThreadInstanz wird hinzugefügt _active, _get_ident()bereits gepatcht, und die gleiche Thread - ID an Bereinigungszeit zurückgegeben. Das ist es!

Um sicherzustellen, dass ich Module in der richtigen Reihenfolge importiere, habe ich meinem Code kurz vor dem Aufruf zum Affen-Patching das folgende Snippet hinzugefügt:

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

Ich hoffe, Sie finden meine Debugging-Geschichte nützlich :)

Codemaler
quelle
5
Tolle Antwort, aber TLDR; Ihre Importe sind in der falschen Reihenfolge, stellen Sie sicher, dass Ihre allerersten Importe import gevent.monkey; gevent.monkey.patch_all()dann sind, was auch immer Sie sonst importieren möchten
cerberos
9
Genau das sage ich oben - die Importreihenfolge ist wichtig.
Code Painters
19

Sie könnten dies verwenden:

import sys
if 'threading' in sys.modules:
    del sys.modules['threading']
import gevent
import gevent.socket
import gevent.monkey
gevent.monkey.patch_all()
user2719944
quelle
Ich mag diesen stillen Ansatz. Aber denken Sie daran, ein import sysüber dem Ausschnitt zu haben :)
Casey
Ich wollte so etwas tun, um später zu versuchen, gevent zu laden. Dinge wie justus.science/blog/2015/04/19/sys.modules-is-dangerous.html
btown
1

Ich hatte ein ähnliches Problem mit einem Gevent-Prototyp-Skript.

Der Greenlet-Rückruf lief einwandfrei und ich synchronisierte über g.join () zurück zum Hauptthread. Für mein Problem musste ich gevent.shutdown () aufrufen, um den Hub herunterzufahren (was ich annehme). Nachdem ich die Ereignisschleife manuell heruntergefahren habe, wird das Programm ohne diesen Fehler ordnungsgemäß beendet.

Kris
quelle
2
+1 - aber ich frage, wie man die Ursache des Problems aufspürt und nicht, wie man das Problem beseitigt.
kkurian
2
Ich sehe das gleiche mit, geventwährend ich Tests mit durchführe nose. Seltsamerweise sehe ich den Fehler nicht, wenn alle Tests bestanden sind, aber wenn ein Test fehlschlägt, sehe ich ihn. Ich benutze monkey.patch_all(). Insbesondere wenn ich das tue, verschwinden monkey.patch_all(thread=False)die Fehler.
Millerdev
Das Aufspüren des Fehlers kann ziemlich schwierig sein. Wenn ich dieses Problem verstehe, hat es mit dem laufenden Hintergrund-Thread zu tun. Es scheint, dass das Problem von der Hauptschleife herrührt, die beendet wird, bevor der Hintergrund-Thread die Fähigkeit hat, das zu beenden, was er tut. Der Interrupt vom Beenden des Hauptthreads muss dazu führen, dass das Programm die Ausnahme auslöst. Ich denke, der beste Weg, um dieses Problem zu lösen, besteht darin, sicherzustellen, dass alle Threads die Verarbeitung abgeschlossen haben, bevor der Hauptprozess heruntergefahren wird.
Kris
@Kris Ich stimme sowohl hinsichtlich der Schwierigkeit als auch hinsichtlich der wahrscheinlichen Ursache des Problems zu. Was ist mir nicht klar, was Fäden abfeuert, was die Fäden tun und warum sie nicht richtig fertig sind? Ich denke, ich gehe einfach davon aus, dass es etwas in gevent ist, dass alles, was ich tue, in Ordnung ist und dass gevent.shutdown () genau das Richtige tut. Danke für Ihre Hilfe!
kkurian
@ Daniel: Vielleicht möchten Sie meinen Beitrag sehen: blog.codepainters.com/2012/11/20/…
Code Painters