我正在使用 py.test 运行一组测试。他们通过。伊皮!但我收到这条消息:
Exception KeyError: KeyError(4427427920,) in <module 'threading' from '/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.pyc'> ignored
我应该如何去追踪它的来源?(我没有直接使用线程,而是使用 gevent。)
我观察到了一个类似的问题,并决定看看到底发生了什么——让我描述一下我的发现。我希望有人会发现它有用。
它确实与猴子修补threading
模块有关。事实上,我可以通过在猴子修补线程之前导入线程模块来轻松触发异常。以下两行就足够了:
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
如果交换导入行,问题就消失了。
我可以在这里停止调试,但我认为值得了解问题的确切原因。
第一步是找到打印有关忽略异常的消息的代码。对我来说找到它有点困难(grepping forException.*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
中的 callable 。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()
call 的职责是加入所有非守护线程并删除主线程(无论这意味着什么)。我决定修补一下 - 用/threading.py
包裹整个_exitfunc()
主体,并用traceback模块打印堆栈跟踪。这给出了以下跟踪:try
except
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
字典将线程 ID(由 返回_get_ident()
)映射到Thread
实例。加载模块时,总是会创建并添加一个类threading
的实例(即使没有显式创建其他线程)。_MainThread
_active
问题是,gevent
猴子补丁修补的方法之一是_get_ident()
- 原始的一个映射到thread.get_ident()
,猴子补丁替换为green_thread.get_ident()
. 显然,这两个调用都为主线程返回了不同的 ID。
现在,如果threading
在猴子修补之前加载了模块,则 call 在创建实例并添加到_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()
我希望你发现我的调试故事很有用:)
你可以使用这个:
import sys
if 'threading' in sys.modules:
del sys.modules['threading']
import gevent
import gevent.socket
import gevent.monkey
gevent.monkey.patch_all()
我对 gevent 原型脚本有类似的问题。
Greenlet 回调执行得很好,我正在通过 g.join() 同步回主线程。对于我的问题,我不得不调用 gevent.shutdown() 来关闭(我假设是)集线器。在我手动关闭事件循环后,程序会正确终止而不会出现该错误。