Gevent 的 KeyError

摘要:

  1. 本文翻譯自 StackOverFlow 上的一篇答案
  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()

希望我的調試經歷能夠對你有用!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章