Я заметил похожую проблему и решил посмотреть, что именно происходит - позвольте мне описать мои выводы. Я надеюсь, что кто-то найдет это полезным.
Рассказ
Это действительно связано с исправлением обезьяны модулем threading
. На самом деле, я могу легко вызвать исключение, импортировав модуль потоков перед тем, как монтировать патчи потоков. Следующие 2 строки достаточно:
import threading
import gevent.monkey; gevent.monkey.patch_thread()
При выполнении выдает сообщение об игнорируемом KeyError
:
(env)czajnik@autosan:~$ python test.py
Exception KeyError: KeyError(139924387112272,) in <module 'threading' from '/usr/lib/python2.7/threading.pyc'> ignored
Если вы поменяете местами строки импорта, проблема исчезнет.
Длинная история
Я мог бы прекратить отладку здесь, но я решил, что стоит понять точную причину проблемы.
Первым шагом было найти код, который печатает сообщение об игнорируемом исключении. Мне было немного трудно его найти (поиск по Exception.*ignored
ничего не дал), но по поиску исходного кода 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
, - _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()
уже исправлен, и тот же поток Идентификатор возвращается во время очистки. Вот и все!
Чтобы убедиться, что я импортирую модули в правильном порядке, я добавил следующий фрагмент кода в свой код, прямо перед вызовом monkey-patching:
import sys
if 'threading' in sys.modules:
raise Exception('threading module loaded before patching!')
import gevent.monkey; gevent.monkey.patch_thread()
Надеюсь, вы найдете мою историю отладки полезной:)