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

Posted

技术标签:

【中文标题】成功运行 py.test 后模块“线程”中的 KeyError【英文标题】:KeyError in module 'threading' after a successful py.test run 【发布时间】:2012-02-05 04:38:14 【问题描述】:

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

【问题讨论】:

【参考方案1】:

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

短篇小说

确实与给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

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

说来话长

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

第一步是找到打印有关忽略异常的消息的代码。对我来说找到它有点困难(grepping for Exception.*ignored 没有产生任何结果),但是在 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() - 值得一看,因为我们知道问题与线程有关。该函数依次调用_shutdown 可在threading 模块中调用。好,我们现在可以回到 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() 时返回另一个值 -因此KeyErrordel _active[_get_ident()]

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

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

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

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

【讨论】:

很好的答案,但是 TLDR;您的导入顺序错误,请确保您的第一个导入是 import gevent.monkey; gevent.monkey.patch_all() 然后是您要导入的任何其他内容 这正是我上面所说的 - 导入顺序很重要。【参考方案2】:

你可以用这个:

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

【讨论】:

我喜欢这种沉默的方法。但请记住在 sn-p 上方有一个import sys :) 我本来想做这样的事情来尝试稍后加载 gevent。 justus.science/blog/2015/04/19/sys.modules-is-dangerous.html 之类的东西【参考方案3】:

我在使用 gevent 原型脚本时遇到了类似的问题。

Greenlet 回调执行良好,我正在通过 g.join() 同步回主线程。对于我的问题,我不得不调用 gevent.shutdown() 来关闭(我假设是)集线器。在我手动关闭事件循环后,程序会正确终止而不会出现该错误。

【讨论】:

+1 -- 但我问的是如何追查问题的根源,而不是如何解决问题。 我在使用gevent 运行测试时看到了同样的情况nose。奇怪的是,当测试全部通过时,我看不到错误,但是当测试失败时,我看到了。我正在使用monkey.patch_all()。值得注意的是,当我执行monkey.patch_all(thread=False) 时,错误就会消失。 追踪错误可能相当困难。如果我理解这个问题,它与正在运行的后台线程有关。似乎问题来自主循环在后台线程有能力完成它正在做的事情之前终止。来自主线程终止的中断必须导致程序抛出异常。我认为解决这个问题的最好方法是在关闭主进程之前确保所有线程都完成了处理。 @Kris 我同意困难和可能导致问题的原因。我不清楚什么是触发线程,线程在做什么,以及为什么它们没有正确完成。我想我只是假设它是 gevent 中的东西,我所做的一切都很好,并且 gevent.shutdown() 只会做正确的事情。感谢您的帮助! @Daniel:你可能想看看我的帖子:blog.codepainters.com/2012/11/20/…

以上是关于成功运行 py.test 后模块“线程”中的 KeyError的主要内容,如果未能解决你的问题,请参考以下文章

Py.test 没有名为 * 的模块

Python:'ImportError:没有命名模块 “

为啥 PyCharm 无法运行单元测试?

在 py.test 中的每个测试之前和之后运行代码?

performSelector withObject afterDelay 在子线程上调用不运行

运行 py.test 时出现错误 ImportMismatchError