Gevent 的 KeyError
文章目录
摘要:
- 本文翻译自 StackOverFlow 上的一篇答案
- 本文主要解释了
gevent
的猴子补丁和一个KeyError
之间的关系
错误描述
在包含有gevent.monkey.patch_thread()
( gevent 的猴子补丁)的程序中,运行时会报出下面的错误:
|
|
解决答案: KeyError in module ’threading’ after a successful py.test run
原文翻译
我观察了同样的主题,然后决定去精确地描述一下到底发生了什么。让我们一起来看一下我的发现,我希望这在以后能够帮助到其他人。
简短的回答
它的确和threading
模块的猴子补丁有关。事实上,我能够轻易地开启这个异常,通过在猴子补丁线程之前导入threading
模块。下面这两行代码就足够了:
|
|
上面的代码执行的时候,就报出了 “忽略了一个KeyError
” 的信息:
|
|
如果你交换一下import
行的顺序,这个错误信息就会消失了。
详细的回答
我可以在这里停止我的调试,但是我觉得它值得让我去了解,造成问题的准确的原因是什么?
第一步是去寻找打印这个忽略了异常的信息的代码。这对于我来说找到这个有点困难(在 python 标准库中 grep 查找Exception .*ignored
没有返回任何东西),但是 grep CPython 的源码,我最终在 Python/error.c 文件中找到了一个函数叫做void PyErr_WriteUnraisable(PyObject *obj)
,它的注释非常有趣,
|
|
我决定去检查谁调用了它,这个利用了gdb
的一点功能来实现的,最终得到了如下的C调用栈,
|
|
现在我们可以清楚地看到异常是在Py_Finalize执行的时候抛出的,这个调用负责关闭Python解释器,释放已经申请的内存等等。它仅仅在退出前调用。
下一步是去查看Py_Finalize()
的代码(它存放在 Python/pythonrun.c )。 它做的非常靠前的一个调用是wait_for_thread_shutdown()
,这个函数非常值得去看一下,因为我们知道问题是关于线程的。
这个函数反过来调用了threading
模块中的_shutdown()
可调用对象,非常好,我们现在可以返回Python代码了。
查看threading.py
,我发现了如下有趣的部分:
|
|
很明显,threading._shutdown()
函数调用的作用就是join所有的非服务化(non daemon)的线程,然后删除主线程(这意味着它确切做了什么)。我决定去给threading.py
打一点补丁,用try / except
包裹整个_exitfunc()
函数体,用traceback
模块来打印出系统调用栈。这个给出了如下的追踪情况:
|
|
现在我们知道了异常抛出的精确位置了,在Thread__delete()
方法内。
接下来的故事在阅读一会threading.py
的代码后就变得很明显。_active
字典将所有已创建的线程的线程ID(由_get_indent()
函数返回)映射到对应的线程实例上。当threading
模块载入的时候, _MainThread
类的实例总是会被创建,而且会被添加到_active
字典中。(甚至没有创建其他线程的时候主线程实例也会创建)。
问题是当一个_get_ident()
方法被gevent
的猴子补丁打过补丁,原来映射的方法thread.get_ident()
被猴子补丁替换成了green_thread.get_ident()
。明显两个函数调用返回的主线程ID并不相同。
现在,如果一个threading
模块在猴子补丁之前被载入,调用_get_ident()
会返回主线程实例创建的时候添加到_active
中的ID。而打上猴子补丁以后就会返回另外一个值,在调用_eixtfunc()
的时候,就会在del _active[_get_ident()]
语句上抛出异常。
与上面的情况相反,如果猴子补丁在threading
模块载入之前被打上了,所有的就都会正常。因为_MainThread
实例被添加到_active
中和_get_ident()
都是在打补丁之后调用的,这样在清理线程的时候就会返回同样的线程ID。就是这样了。
为了确保以正确的顺序导入模块,我在我的电脑中添加了如下的代码片段,仅仅在打上猴子补丁之前调用:
|
|
希望我的调试经历能够对你有用!