成功运行py.test后模块'threading'中的KeyError

68 投票
3 回答
31853 浏览
提问于 2025-04-17 09:46

我正在用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。)

3 个回答

1

我之前也遇到过类似的问题,使用的是一个gevent的原型脚本。

在这个脚本中,Greenlet的回调函数运行得很好,我通过g.join()把结果同步回主线程。对于我的问题,我需要调用gevent.shutdown()来关闭(我猜是)Hub。在我手动关闭事件循环后,程序就能正常结束,没有出现那个错误。

19

你可以使用这个:

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

我遇到了一个类似的问题,决定深入了解一下情况——让我来分享我的发现。希望对某些人有帮助。

简短的故事

这个问题确实和对 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

如果你把导入的顺序调换一下,问题就解决了。

长篇故事

我本可以在这里停止调试,但我觉得有必要了解问题的确切原因。

第一步是找到打印关于被忽略异常消息的代码。对我来说,这有点难(搜索 Exception.*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()——值得关注,因为我们知道问题和线程有关。这个函数又调用了 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() 的职责是连接所有非守护线程并删除主线程(这具体是什么意思不太清楚)。我决定稍微修改一下 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_ident() 返回)映射到所有创建的 Thread 实例。当 threading 模块被加载时,总是会创建一个 _MainThread 类的实例并添加到 _active 中(即使没有显式创建其他线程)。

问题在于,gevent 的猴子补丁修改了 _get_ident() 方法——原来的方法映射到 thread.get_ident(),而猴子补丁将其替换为 green_thread.get_ident()。显然,这两个调用返回的主线程 ID 是不同的。

现在,如果在猴子补丁之前加载 threading 模块,_get_ident() 在创建并添加 _MainThread 实例到 _active 时返回一个值,而在调用 _exitfunc() 时返回另一个值——因此在 del _active[_get_ident()] 中出现 KeyError

相反,如果在加载 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()

希望我的调试故事对你有帮助 :)

撰写回答