py.test成功运行后,关于模块” threading”中的python:KeyError

KeyError in module 'threading' after a successful py.test run

我正在使用py.test运行一组测试。他们通过了。 pp!但我收到此消息:

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

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


我观察到类似的问题,并决定确切地了解发生了什么-让我描述一下我的发现。我希望有人会发现它有用。

短篇故事

它确实与猴子修补threading模块有关。实际上,通过在猴子修补线程之前导入线程模块,我可以轻松触发异常。以下两行就足够了:

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

执行时会吐出有关忽略的KeyError

的消息

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

如果交换导入行,问题就消失了。

很长的故事

我可以在这里停止调试,但是我认为值得了解问题的确切原因。

第一步是找到打印有关被忽略异常的消息的代码。我很难找到它(对Exception.*ignored进行尝试没有产生任何结果),但是围绕CPython源代码进行了grepping,我最终在Python / error.c中找到了一个名为void PyErr_WriteUnraisable(PyObject *obj)的函数,并带有一个非常有趣的注释:

1
2
/* 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级堆栈跟踪:

1
2
3
4
5
6
7
#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()-值得研究,因为我们知道问题与线程有关。该函数依次调用threading模块中可调用的_shutdown。好的,我们现在可以回到python代码。

看着threading.py,我发现了以下有趣的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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进行一些修补-用try / except包裹整个_exitfunc()主体,并使用traceback模块打印堆栈跟踪。这给出了以下跟踪:

1
2
3
4
5
6
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模块,则在创建_MainThread实例并将其添加到_active时,_get_ident()调用将返回一个值,而在_exitfunc()时调用另一个值-因此del _active[_get_ident()]中的KeyError

相反,如果在threading加载之前完成了猴子补丁,那么一切都很好-在将_MainThread实例添加到_active时,已经对_get_ident()进行了补丁,并且具有相同的线程ID在清理时返回。就是这样!

为确保以正确的顺序导入模块,我在猴子补丁调用之前将以下代码段添加到了我的代码中:

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

我希望您发现我的调试故事很有用:)


您可以使用此:

1
2
3
4
5
6
7
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()来关闭(假设是)集线器。在我手动关闭事件循环后,程序会正确终止,而不会出现该错误。