运行成功的py.test后,模块“线程”中的KeyError

我正在用py.test进行一系列testing。 他们通过。 开心辞典! 但是我收到这个消息:

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

我应该如何追踪这个来源呢? (我不是直接使用线程,而是使用gevent。)

我观察到类似的问题,并决定看看究竟发生了什么 – 让我来描述一下我的发现。 我希望有人会觉得有用。

短篇故事

这确实与猴子修补threading模块有关。 事实上,我可以通过在monkey-patching线程之前导入线程模块来轻松触发exception。 以下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 

如果换了import线,问题就没有了。

很长的故事

我可以在这里停止我的debugging,但我决定了解这个问题的确切原因是值得的。

第一步是find打印关于被忽略的exception的消息的代码。 对于我来说find它有点困难(grep for Exception.*ignored任何东西),但是仔细考虑CPython源代码我最终在Python / error.c中find了一个名为void PyErr_WriteUnraisable(PyObject *obj) 函数 ,一个非常有趣的评论:

 /* 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执行时引发的exception – 这个调用负责closuresPython解释器,释放分配的内存等等。它就在退出之前调用。

下一步是看Py_Finalize()代码(它在Python / pythonrun.c中 )。 它所做的第一个调用是wait_for_thread_shutdown() – 值得关注,因为我们知道这个问题与线程有关。 这个函数又调用了在threading模块中调用的_shutdown 。 好,我们现在可以回到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()调用的责任是join所有非守护进程线程并删除主线程(不pipe是什么意思)。 我决定修补threading.py有点 – 用try / except包装整个_exitfunc()体,并用跟踪模块打印堆栈跟踪。 这给了以下的痕迹:

 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 

现在我们知道引发exception的确切位置 – 在Thread.__delete()方法内部。

在阅读threading.py一段时间之后,其余的部分是显而易见的。 对于创build的所有线程, _active字典将线程ID(由_get_ident()返回_get_ident()映射到Thread实例。 加载threading模块时,总是创build_MainThread类的一个实例,并将其添加到_active (即使没有显式创build其他线程)。

问题是gevent的monkey-patching修补的方法之一是_get_ident() – 原始的映射到thread.get_ident() ,monkey-patching用green_thread.get_ident()replace它。 显然这两个调用都会为主线程返回不同的ID。

现在,如果在monkey-patching之前加载了threading模块,当_MainThread实例被创build并添加到_active时, _get_ident()调用返回一个值,并调用_exitfunc()时间的另一个值 – 因此, del _active[_get_ident()]

相反,如果在加载threading之前完成了monkey-patching,则一切正常 – 当_MainThread实例被添加到_active_get_ident()已经被修补,并且在清理时返回相同的线程ID。 而已!

为了确保我以正确的顺序导入模块,我在我的代码中添加了以下片段,就在monkey-patching调用之前:

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

我希望你find我的debugging故事有用:)

你可以使用这个:

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

我和一个gevent原型脚本有类似的问题。

Greenletcallback执行正常,我通过g.join()同步回主线程。 对于我的问题,我不得不调用gevent.shutdown()closures(我认为是)集线器。 在我手动closures事件循环后,程序正常终止,没有错误。