在服務(wù)器上程序中遇到一個(gè) import
卡死的情況惑折,而且這個(gè) bug 只能在服務(wù)器上重現(xiàn)术裸,我的電腦上不會(huì)重現(xiàn)。去掉一些無用的代碼停团,可以抽象出如下的代碼旷坦。
bar.py
# coding=utf-8
from threading import Thread
class Bar(Thread):
def run(self):
u"知乎".encode("utf-8")
bar = Bar()
bar.start()
bar.join()
foo.py
import bar
然后直接執(zhí)行 python foo.py
,這時(shí)程序卡死不動(dòng)了佑稠。
首先必須要知道的是程序卡在哪里了秒梅,所以使用 trace 模塊去看程序的執(zhí)行流程。
執(zhí)行 python -m trace -t foo.py
舌胶,這是程序調(diào)用的最后的部分捆蜀。
__init__.py(93): for modname in modnames:
__init__.py(94): if not modname or '.' in modname:
__init__.py(96): try:
__init__.py(99): mod = __import__('encodings.' + modname, fromlist=_import_tail,
__init__.py(100): level=0)
threading.py(237): waiter = _allocate_lock()
threading.py(238): waiter.acquire()
threading.py(239): self.__waiters.append(waiter)
threading.py(240): saved_state = self._release_save()
--- modulename: threading, funcname: _release_save
threading.py(220): self.__lock.release() # No state to save
threading.py(241): try: # restore state no matter what (e.g., KeyboardInterrupt)
threading.py(242): if timeout is None:
threading.py(243): waiter.acquire()
我們很明顯的看到了程序是卡在了獲得鎖的時(shí)候,但是我的程序里沒有明確的加鎖啊,為什么出現(xiàn)這種情況呢辆它?通過調(diào)用記錄向上追溯看到
mod = __import__('encodings.' + modname, fromlist=_import_tail, level=0)
是這一步引入了最后的鎖誊薄,發(fā)現(xiàn)包含這行代碼的文件是 /usr/lib/python2.7/encodings/__init__.py
,大致猜出是執(zhí)行u"知乎".encode("utf-8")
卡死的娩井。
現(xiàn)在再看 __import__
的實(shí)現(xiàn),發(fā)現(xiàn) PyImport_ImportModuleLevel
調(diào)用了 _PyImport_AcquireLock
似袁,當(dāng) import_module_level
成功后調(diào)用 _PyImport_ReleaseLock
洞辣。
PyObject *
PyImport_ImportModuleLevel(char *name, PyObject *globals, PyObject *locals,
PyObject *fromlist, int level)
{
PyObject *result;
_PyImport_AcquireLock();
result = import_module_level(name, globals, locals, fromlist, level);
if (_PyImport_ReleaseLock() < 0) {
Py_XDECREF(result);
PyErr_SetString(PyExc_RuntimeError,
"not holding the import lock");
return NULL;
}
return result;
}
再去繼續(xù)看 _PyImport_AcquireLock 的代碼可以明顯的看到有一個(gè) import_lock
存在。也就是 import
的時(shí)候會(huì)引入import_lock
, 當(dāng)我們 import bar
的時(shí)候昙衅,首先會(huì)獲得 import_lock
扬霜,但是當(dāng)我們執(zhí)行到 mod = __import__('encodings.' + modname, fromlist=_import_tail, level=0)
的時(shí)候新創(chuàng)建的線程會(huì)再次請(qǐng)求獲得這把import_lock
。在一把鎖內(nèi)部而涉,再次請(qǐng)求獲得這把鎖造成了死鎖著瓶,使程序直接卡住了。在服務(wù)器上把u"知乎".encode("utf-8")
換成 import socket
照樣會(huì)卡在 import_lock
處啼县。
通過分析材原,現(xiàn)在終于找出原因了。但是為什么只能在服務(wù)上重現(xiàn)呢季眷?為什么本地的機(jī)器沒有問題余蟹?
我把 u"知乎".encode("utf-8")
換成 import socket
,在本地執(zhí)行也會(huì)卡在 import_lock
子刮。那為什么 u"知乎".encode("utf-8")
為啥在本地不卡呢威酒。那就用 ipdb
看看u"知乎".encode("utf-8")
在本地和服務(wù)器上的調(diào)用有啥不同吧。
#coding=utf-8
import ipdb
ipdb.set_trace()
u"知乎".encode("utf-8")
結(jié)果發(fā)現(xiàn)在本地根本就沒有進(jìn)入 search_function
挺峡,程序執(zhí)行完葵孤。而在服務(wù)器上直接進(jìn)入了 /usr/lib/python2.7/encodings/__init__.py
文件作郭,逐步的執(zhí)行到 __import__
造成了死鎖市埋。為什么本地的機(jī)器上不用加載呢采呐?
在本地的 encodings/init.py 文件里加上調(diào)試信息 print encoding
饮睬,發(fā)現(xiàn)在本地直接輸入 python
啟動(dòng)命令行胎食,直接就打印出了 utf-8
危队,而在服務(wù)器上是 ascii
习贫。原來不同的機(jī)器上加載的默認(rèn)編碼不一樣歉井。通過 locale.getdefaultlocale
也發(fā)現(xiàn)默認(rèn)的編碼服務(wù)器默認(rèn)的編碼是 ascii
送挑,本地是 utf-8
绑莺。
終于知道了原來根據(jù)環(huán)境不同,默認(rèn)加載的編碼是不一樣的惕耕,加載了的編碼會(huì)有 cache 就不用執(zhí)行到 __import__
纺裁,沒有加載過的編碼就會(huì)執(zhí)行。我又在自己的服務(wù)器上把 encode("utf-8")
改成encode("utf8")
,發(fā)現(xiàn)本地的程序也卡在了 __import__
的地方欺缘。
至此這個(gè) bug栋豫,終于搞清楚了,真是艱難谚殊。簡(jiǎn)單總結(jié)下
一般在最外層只寫函數(shù)丧鸯,類,變量定義代碼嫩絮。其它有副作用的代碼都放到函數(shù)里丛肢,尤其不能在最外層寫 Thread.join 這種會(huì) block 住整個(gè)程序運(yùn)行的代碼。import
完之后再顯式調(diào)用函數(shù)來執(zhí)行這些代碼剿干。 原則是使 import
盡量不帶有副作用蜂怎。
這個(gè)問題得以解決,絕大部分的功勞屬于安江澤置尔。同時(shí)感謝Leo Jay 的指正杠步。