欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Gevent 的 KeyError

程序员文章站 2024-03-24 23:10:34
...

摘要:

  1. 本文翻译自 * 上的一篇答案
  2. 本文主要解释了gevent的猴子补丁和一个KeyError之间的关系

错误描述

在包含有gevent.monkey.patch_thread()( gevent 的猴子补丁)的程序中,运行时会报出下面的错误:

Exception KeyError: KeyError(140468381321488,) in <module 'threading' from '/usr/lib/python2.7/threading.pyc'> ignored

解决答案: KeyError in module ‘threading’ after a successful py.test run

原文翻译

我观察了同样的主题,然后决定去精确地描述一下到底发生了什么。让我们一起来看一下我的发现,我希望这在以后能够帮助到其他人。

简短的回答

它的确和threading模块的猴子补丁有关。事实上,我能够轻易地开启这个异常,通过在猴子补丁线程之前导入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

如果你交换一下import行的顺序,这个错误信息就会消失了。

详细的回答

我可以在这里停止我的调试,但是我觉得它值得让我去了解,造成问题的准确的原因是什么?

第一步是去寻找打印这个忽略了异常的信息的代码。这对于我来说找到这个有点困难(在 python 标准库中 grep 查找Exception .*ignored没有返回任何东西),但是 grep CPython 的源码,我最终在 Python/error.c 文件中找到了一个函数叫做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执行的时候抛出的,这个调用负责关闭Python解释器,释放已经申请的内存等等。它仅仅在退出前调用。

下一步是去查看Py_Finalize()的代码(它存放在 Python/pythonrun.c )。 它做的非常靠前的一个调用是wait_for_thread_shutdown(),这个函数非常值得去看一下,因为我们知道问题是关于线程的。

这个函数反过来调用了threading模块中的_shutdown()可调用对象,非常好,我们现在可以返回Python代码了。

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

class _MainThread(Thread):
    def _exitfunc(self):
        self._Thread__stop()
        t = _pickSomeNonDaemonThrad()
        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所有的非服务化(non daemon)的线程,然后删除主线程(这意味着它确切做了什么)。我决定去给threading.py打一点补丁,用try / except包裹整个_exitfunc()函数体,用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字典将所有已创建的线程的线程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。就是这样了。

为了确保以正确的顺序导入模块,我在我的电脑中添加了如下的代码片段,仅仅在打上猴子补丁之前调用:

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

希望我的调试经历能够对你有用!

相关标签: stack overflow