奇怪的 dead lock

在服务器上程序中遇到一个 import 卡死的情况,而且这个 bug 只能在服务器上重现,我的电脑上不会重现。去掉一些无用的代码,可以抽象出如下的代码。

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

然后直接执行 python foo.py,这时程序卡死不动了。

首先必须要知道的是程序卡在哪里了,所以使用 trace 模块去看程序的执行流程。
执行 python -m trace -t foo.py,这是程序调用的最后的部分。

__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()

我们很明显的看到了程序是卡在了获得锁的时候,但是我的程序里没有明确的加锁啊,为什么出现这种情况呢?通过调用记录向上追溯看到
mod = __import__('encodings.' + modname, fromlist=_import_tail, level=0)
是这一步引入了最后的锁,发现包含这行代码的文件是 /usr/lib/python2.7/encodings/__init__.py,大致猜出是执行u"知乎".encode("utf-8")卡死的。

现在再看 __import__ 的实现,发现 PyImport_ImportModuleLevel 调用了 _PyImport_AcquireLock,当 import_module_level 成功后调用 _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;
}

再去继续看 _PyImport_AcquireLock 的代码可以明显的看到有一个 import_lock 存在。也就是 import 的时候会引入import_lock, 当我们 import bar 的时候,首先会获得 import_lock,但是当我们执行到 mod = __import__('encodings.' + modname, fromlist=_import_tail, level=0)的时候新创建的线程会再次请求获得这把import_lock。在一把锁内部,再次请求获得这把锁造成了死锁,使程序直接卡住了。在服务器上把u"知乎".encode("utf-8") 换成 import socket 照样会卡在 import_lock 处。

通过分析,现在终于找出原因了。但是为什么只能在服务上重现呢?为什么本地的机器没有问题?

我把 u"知乎".encode("utf-8") 换成 import socket ,在本地执行也会卡在 import_lock。那为什么 u"知乎".encode("utf-8") 为啥在本地不卡呢。那就用 ipdb 看看u"知乎".encode("utf-8")在本地和服务器上的调用有啥不同吧。

 #coding=utf-8
  
 import ipdb
 ipdb.set_trace()
 u"知乎".encode("utf-8")

结果发现在本地根本就没有进入 search_function,程序执行完。而在服务器上直接进入了 /usr/lib/python2.7/encodings/__init__.py 文件,逐步的执行到 __import__ 造成了死锁。为什么本地的机器上不用加载呢?

在本地的 encodings/init.py 文件里加上调试信息 print encoding,发现在本地直接输入 python 启动命令行,直接就打印出了 utf-8,而在服务器上是 ascii。原来不同的机器上加载的默认编码不一样。通过 locale.getdefaultlocale 也发现默认的编码服务器默认的编码是 ascii,本地是 utf-8

终于知道了原来根据环境不同,默认加载的编码是不一样的,加载了的编码会有 cache 就不用执行到 __import__,没有加载过的编码就会执行。我又在自己的服务器上把 encode("utf-8") 改成encode("utf8"),发现本地的程序也卡在了 __import__ 的地方。

至此这个 bug,终于搞清楚了,真是艰难。简单总结下

一般在最外层只写函数,类,变量定义代码。其它有副作用的代码都放到函数里,尤其不能在最外层写 Thread.join 这种会 block 住整个程序运行的代码。import 完之后再显式调用函数来执行这些代码。 原则是使 import 尽量不带有副作用。

这个问题得以解决,绝大部分的功劳属于安江泽。同时感谢Leo Jay 的指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,779评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,993评论 19 139
  • 字符集和编码简介 在编程中常常可以见到各种字符集和编码,包括ASCII,MBCS,Unicode等字符集。确切的说...
    兰山小亭阅读 8,633评论 0 13
  • http://python.jobbole.com/85231/ 关于专业技能写完项目接着写写一名3年工作经验的J...
    燕京博士阅读 7,631评论 1 118
  • 十月已经快过完了,我在这一个月做了什么有什么收获。 气温的骤降让我的的热情也下降了许多,不管是对生活,学习还是减肥...
    A_insist阅读 470评论 0 0