摘要:
- 本文翻譯自 StackOverFlow 上的一篇答案
- 本文主要解釋了
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()
希望我的調試經歷能夠對你有用!