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的调用,用于造成一个死锁的情况。
7 再来看看测试的例子:
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 下面来看看如何查找这个死锁问题
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调试命令了,但是个人觉得还是这种方法好用些。