Python死锁问题查找
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调试命令了,但是个人觉得还是这种方法好用些。
更多推荐
所有评论(0)