1 用Python写的程序,由于其GIL的存在,死锁的情况变得有点复杂。

2 基本上可以分成两种情况:

一种是在调用Thread.join,Queue.put,Queue.get,Lock.acquire这些函数造成的死锁,可以归结成Python代码层造成的死锁

另一种情况是由于无法获取GIL而造成的死锁,可以归结成C代码层造成的死锁,具体原因是这样的:

由于某个Python线程调用了一个C函数,而这个函数在等某一资源的释放,并且Python线程在调用这个函数之前又没有把GIL给释放掉,一直占着GIL,

造成其它的Python线程因无法获取GIL都处于死锁状态

因为这时候Python代码已经无法执行,用pydev,winpdb等工具已经无能为力了。

3 前一种情况的死锁比较好查,用pydev,winpdb查看下堆栈就可以分析出死锁的具体位置。

4 由于第一种情况的死锁原因查起来比较简单,这里重点讨论一下第二种情况的死锁问题的查找。

5 按照惯例,照样先举一个例子,这个例子从《Cython进阶--用Cython封装Callback函数》里的示例代码修改而来。

6 改动很小,只是在JoinThread这个函数里去掉了PyEval_SaveThread和PyEval_RestoreThread的调用,用于造成一个死锁的情况。


cdef extern from "pthread.h":

    ctypedef void *pthread_t

 

    ctypedef structpthread_attr_t:

       pass

 

    int pthread_create(pthread_t *__newthread, \

                  pthread_attr_t *__attr, \

                  void *(*__start_routine) (void *) except*, \

                  void *__arg)

                   

    int pthread_join(pthread_t __th, void **__thread_return)

 

 

cdef extern from "Python.h":

    ctypedef enumPyGILState_STATE:

       pass

    ctypedef enumPyThreadState:

       pass

 

    PyGILState_STATEPyGILState_Ensure()

    voidPyGILState_Release(PyGILState_STATE)

 

    PyThreadState *PyEval_SaveThread()

    voidPyEval_RestoreThread(PyThreadState *)

 

    void PyEval_InitThreads()

 

    void Py_INCREF(objectobj)

    void Py_DECREF(objectobj)

 

def Callback(obj):

    if hasattr(obj,'run'):

       obj.run()

 

cdef void * start (void * param) except*:

    cdef PyGILState_STATEstate

    cdef object obj =0

    state =PyGILState_Ensure()

    Callback(obj)

    Py_DECREF(obj)

   PyGILState_Release(state)

    return 1

 

def CreateThread(obj):

    cdef int ret

    cdef pthread_t tid

    PyEval_InitThreads()

 

    Py_INCREF(obj)

    ret = pthread_create(&tid, 0, start,obj)

    if ret != -1:

       return tid

    else:

       Py_DECREF(obj)

       return None

 

def JoinThread(int tid):

    cdef PyThreadState *state

    cdef void *thread_return= 0

    cdef int ret

#    state = PyEval_SaveThread()

    ret =pthread_join(tid,&thread_return)

#    PyEval_RestoreThread(state)

    if ret != -1:

       return thread_return

    else:

       return 0




7 再来看看测试的例子:


callme.py

#!/usr/bin/python

import callback

import time

 

class Thread:

    def __init__(self):

       self.tid = None

    def start(self):

       self.tid = callback.CreateThread(self) #(self)

    def run(self):

       pass

    def join(self):

       callback.JoinThread(self.tid)

 

class MyThread1(Thread):

    def run(self):

       for i in xrange(10):

           print 'output from thread 1:',i

 

 

class MyThread2(Thread):

    def run(self):

       for i in xrange(10):

           print 'output from thread 2:',i

 

def test(n):

    for i in range(n):

       t1 = MyThread1()

       t1.start()

       t1.join()

 

 

test(3)


8 首先,要确保使用的python是用O0优化级别编译的,如果是用O2或者O3级别编译的,会因为优化对问题的查找带来麻烦

9 在ubuntu上,用apt-get 命令安装的python应该是用O0级别编译的。

10 如果你用的编译不是,那么就要在编译的时候执行如下编译:make OPT=-O0

11 ./callme.py& 运行这个例子(&表示后台运行),会发现程序没有输出,也没有退出,死锁了。

12 这个测试用例首先启动一个线程,然后马上调用join方法等待线程线出。

13 初看好像没什么问题,但是对GIL有点认识的人就会知道,该程序有很大的问题,join在等线程结束,而线程在执行python代码时却获取不到GIL而无法执行,从而造成了死锁。

14 下面来看看如何查找这个死锁问题

 


# ./callme.py &

[1] 1283

 

# gdb --pid 1283

GNU gdb (GDB) 7.1-ubuntu

Copyright (C) 2010 Free SoftwareFoundation, Inc.

License GPLv3+: GNU GPL version 3 or later

This is free software: you are free tochange and redistribute it.

There is NO WARRANTY, to the extentpermitted by law.  Type "showcopying"

and "show warranty" for details.

This GDB was configured as"i486-linux-gnu".

For bug reporting instructions, please see:

.

Attaching to process 1283

Reading symbols from /home/flyfish/Python-2.7.2/bin/python2.7...done.

Reading symbols from/lib/tls/i686/cmov/libpthread.so.0...(no debugging symbols found)...done.

[Thread debugging using libthread_dbenabled]

[New Thread 0xb6fe0b70 (LWP 1285)]

[New Thread 0xb77e1b70 (LWP 1284)]

Loaded symbols for/lib/tls/i686/cmov/libpthread.so.0

...

Reading symbols from /home/flyfish/Python-2.7.2/lib/python2.7/lib-dynload/time.so...done.

Loaded symbols for /home/flyfish/Python-2.7.2/lib/python2.7/lib-dynload/time.so

0x00ecc422 in __kernel_vsyscall ()

(gdb) bt

#0 0x00ecc422 in__kernel_vsyscall ()

#1 0x00898b5d in pthread_join () from /lib/tls/i686/cmov/libpthread.so.0

#2 0x0084d529 in__pyx_pf_8callback_4JoinThread (__pyx_self=0x0, __pyx_arg_tid=0x9545174) atcallback.c:1046

#3 __pyx_pw_8callback_5JoinThread (__pyx_self=0x0, __pyx_arg_tid=0x9545174)at callback.c:1005

#4 0x080e621b in call_function (f=0x9593d04, throwflag=0) at Python/ceval.c:4001

#5 PyEval_EvalFrameEx (f=0x9593d04, throwflag=0) at Python/ceval.c:2666

#6 0x080e63fd in fast_function (f=0x9531b14, throwflag=0) atPython/ceval.c:4099

#7 call_function (f=0x9531b14, throwflag=0) at Python/ceval.c:4034

#8 PyEval_EvalFrameEx (f=0x9531b14, throwflag=0) at Python/ceval.c:2666

#9 0x080e6e69 inPyEval_EvalCodeEx (co=0xb78388d8, globals=0xb78da35c, locals=0xb78da35c, args=0x0,

    argcount=0, kws=0x0,kwcount=0, defs=0x0, defcount=0, closure=0x0) at Python/ceval.c:3253

#10 0x080e6f97 inPyEval_EvalCode (co=0xb78388d8, globals=0xb78da35c, locals=0xb78da35c) at Python/ceval.c:667

#11 0x08104d1d in run_mod (fp=0x9531b08,filename=0xbffcc922 "./callme.py", start=257, globals=0xb78da35c,

    locals=0xb78da35c, closeit=1, flags=0xbffcbeec) atPython/pythonrun.c:1346

#12 PyRun_FileExFlags (fp=0x9531b08,filename=0xbffcc922 "./callme.py", start=257, globals=0xb78da35c,

    locals=0xb78da35c, closeit=1, flags=0xbffcbeec) atPython/pythonrun.c:1332

#13 0x08104f82 inPyRun_SimpleFileExFlags (fp=0x9531b08, filename=0xbffcc922"./callme.py", closeit=1,

    flags=0xbffcbeec) atPython/pythonrun.c:936

#14 0x0805ba0f in Py_Main (argc=2, argv=0xbffcbfd4) atModules/main.c:599

#15 0x0805ab1b in main (argc=2,argv=0xbffcbfd4) at ./Modules/python.c:23

(gdb) source gdbinit

 

(gdb) frame 8

#8 PyEval_EvalFrameEx (f=0x949fa74, throwflag=0) at Python/ceval.c:2666

2666              x = call_function(&sp, oparg);

(gdb) pyframe

./callme.py (30): test

(gdb) pylocals

n:

object : 3

type    : int

refcount: 26

address : 0x942e098

i:

object : 0

type    : int

refcount: 310

address : 0x942e0bc

t1:

object : <__main__.MyThread1 instance at 0xb765eb6c>

type    : instance

refcount: 5

address : 0xb765eb6c


15 解释一下:

gdb --pid 1283

用gdb attach到python进程,如果不知道进程的PID,也可以用ps -A | grep callme.py来查看进程的PID,callme.py为进程的名称,也可以用pidof 本例中,由于后台运行时已经打印出了pid,所以不用再执行该命令了。

bt

查看当前线程的堆栈

source gdbinit

加载python的调试命令,gdbinit为gdb的调试代码,在Python源码包里的Misc目录下有这个文件,可以把这个文件拷贝到当前目录下,也可以在source时直接指定gdbinit的绝对路径。

frame 8

通过bt命令已经知道了堆栈,通过堆栈我们知道了在堆栈的frame 8 的位置调用了PyEval_EvalFrameEx这个函数

这里要说明一下,在调用PyEval_EvalFrameEx时,实际上就是执行了一条python代码

通过frame 8这条命令可以定位到这个函数所在的堆栈。

pyframe

这条命令就是通过source  gdbinit加载的命令,通过执行这条命令,会打印出当前堆栈执行的是哪一条python代码

通过重复执行frame和pyframe,就可以知道python代码的整个堆栈。

pylocals

这条命令也是在gdbinit中定义的一条命令,可以查看当前python堆栈使用的局部变量,

例如本例中通过该条命令可以很方便的知道输入的n的值为3

16 对于有多个python线程的情况,如果要切换到其它线程,

可以用info thread查看所有线程

再用thread thread_id命令来切换到其它线程。

17 基本上,通过thread+bt+frame+pyframe+pylocals这五条命令就可以定位到死锁的位置。

18 当然,gdbinit中还定义了其它有用的命令,有兴趣的可以试试。

19 另外,gdb-7已经直接添加了python调试的支持,为python代码的调试提供了更加强大和方便的功能,不需要用sourcegdbinit这样的命令来加载Python调试命令了,但是个人觉得还是这种方法好用些。

 

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐