Ответ 1
Я наблюдал подобную проблему и решил посмотреть, что происходит в точности - позвольте мне описать мои выводы. Я надеюсь, что кто-то найдет это полезным.
Рассказ
Это действительно связано с обезвреживанием модуля threading
. Фактически, я могу легко вызвать исключение, импортировав модуль потоковой передачи перед потоками обезглавливания. Достаточно двух следующих строк:
import threading
import gevent.monkey; gevent.monkey.patch_thread()
При выполнении он выплескивает сообщение об игнорированном KeyError
:
(env)[email protected]:~$ python test.py
Exception KeyError: KeyError(139924387112272,) in <module 'threading' from '/usr/lib/python2.7/threading.pyc'> ignored
Если вы замените строки импорта, проблема исчезнет.
Длинная история
Я мог бы остановить мою отладку здесь, но я решил, что стоит разобраться в конкретной причине проблемы.
Первым шагом было найти код, который выводит сообщение об игнорированном исключении. Мне было трудно найти его (grepping для Exception.*ignored
ничего не дал), но grepping вокруг исходного кода CPython. В итоге я нашел функцию под названием void PyErr_WriteUnraisable(PyObject *obj)
в Python/error.c, с очень интересным комментарием:
/* Call when an exception has occurred but there is no way for Python
to handle it. Examples: exception in __del__ or during GC. */
Я решил проверить, кто это вызывает, с небольшой помощью от gdb
, чтобы получить следующую трассировку стека на уровне C:
#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 ()
Теперь мы можем ясно видеть, что исключение выбрано при выполнении Py_Finalize - этот вызов отвечает за закрытие интерпретатора Python, освобождение выделенной памяти и т.д. Он назывался непосредственно перед выходом из строя.
Следующим шагом было посмотреть код Py_Finalize()
(он находится в Python/pythonrun.c). Самый первый вызов, который он делает, - это wait_for_thread_shutdown()
- стоит посмотреть, поскольку мы знаем, что проблема связана с потоковой обработкой. Эта функция, в свою очередь, вызывает _shutdown
, вызываемый в модуле threading
. Хорошо, теперь мы можем вернуться к коду python.
Глядя на threading.py
, я нашел следующие интересные части:
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
Очевидно, что ответственность за вызов threading._shutdown()
заключается в объединении всех потоков не-демона и удалении основного потока (что бы это ни говорило точно). Я решил немного скопировать threading.py
- обернуть тело _exitfunc()
целым try
/except
и распечатать трассировку стека с помощью traceback модуля. Это дало следующий след:
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
Теперь мы знаем точное место, где выбрано исключение - внутри метода Thread.__delete()
.
Остальная часть истории очевидна после прочтения threading.py
на некоторое время. Словарь _active
сопоставляет идентификаторы потоков (возвращаемые _get_ident()
) экземплярами Thread
для всех созданных потоков. Когда модуль threading
загружен, экземпляр класса _MainThread
всегда создается и добавляется в _active
(даже если никакие другие потоки явно не созданы).
Проблема в том, что один из методов, исправленных gevent
monkey-patching, _get_ident()
- исходный, сопоставляется с thread.get_ident()
, исправление обезьяны заменяет его на green_thread.get_ident()
. Очевидно, что оба вызова возвращают разные идентификаторы для основного потока.
Теперь, если модуль threading
загружен перед патчем обезьяны, вызов _get_ident()
возвращает одно значение, когда экземпляр _MainThread
создается и добавляется в _active
, а другое значение в момент _exitfunc()
вызывается - следовательно, KeyError
в del _active[_get_ident()]
.
Наоборот, если обезвреживание обезьяны выполняется до загрузки threading
, все в порядке - во время добавления экземпляра _MainThread
к _active
, _get_ident()
уже исправлен, а тот же поток ID возвращается во время очистки. Что это!
Чтобы убедиться, что я импортирую модули в правильном порядке, я добавил следующий код в мой код, перед тем, как вызов обезьян-патчей:
import sys
if 'threading' in sys.modules:
raise Exception('threading module loaded before patching!')
import gevent.monkey; gevent.monkey.patch_thread()
Надеюсь, вы найдете полезную историю отладки:)