Back to home

多线程与多进程混搭惹的祸

初次发表于2015/4/9

【背景】
python写稍微复杂的后台程序时,曾经留下一点文章。
按照当时的思路,选用了多进程的方式来实现并发。
很多人都说过python下尽量少用线程,个人觉得python的线程并不是洪水猛兽,有节制的使用倒是无妨。
因而,程序主体用的进程池,同时,对于一些轻量级的需要异步处理的逻辑,如对外通信,结果处理等也用到了多线程。
该程序启动起来,进程树大致是这个样子:
python(12691)─┬─python(12720)─┬─python(12721)
├─python(12723)
└─{python}(12722)
├─{python}(12716)
├─{python}(12717)
├─{python}(12718)
├─{python}(12719)
├─{python}(12724)
├─{python}(12725)
├─{python}(12726)
└─{python}(12727)
绿色的进程号是专门创建进程池的进程的进程,红色的进程号就是进程池中的进程,紫色的就是用来处理异步逻辑的线程。
【问题】
最近,程序运行过程中出现了死锁的现象,顺藤摸瓜一步一步往下排查:
1. 进程池中所有的进程均死锁
python(5983)─┬─python(6066)─┬
├─python(29521)
├─python(29551)
└─{python}(6068)
├─{python}(6044)
├─{python}(6045)
├─{python}(6046)
├─{python}(6047)
├─{python}(6048)
├─{python}(6078)
├─{python}(6079)
├─{python}(6080)
└─{python}(6086)
# strace -p 32375
Process 32375 attached
futex(0x96462f8, FUTEX_WAIT_PRIVATE, 0, NULL
2. 查看具体的进程,锁被线程id-12656364960xB48FEB70)的线程获取
WVSS log # gdb -p 29551
(gdb) py-bt
#5 Frame 0x965bb54, for file
/opt/6030/13674/wse/env/python/lib/python2.7/threading.py, line 127, in acquire
(self=<_RLock(_Verbose__verbose=False, _RLock__owner=-1265636496,
_RLock__block=, _RLock__count=1) at remote
0xb70221ec>, blocking=1, me=-1218853184)
3. 12656364960xb48feb70找到该线程,为主进程下的通信线程6047
WVSS log # gdb -p 5983
(gdb) info threads
Id Target Id Frame
10 Thread 0xb62f5b70 (LWP 6044) "python" 0xb774d424 in __kernel_vsyscall ()
9 Thread 0xb5af4b70 (LWP 6045) "python" 0xb774d424 in __kernel_vsyscall ()
8 Thread 0xb50ffb70 (LWP 6046) "python" 0xb774d424 in __kernel_vsyscall ()
7 Thread 0xb48feb70 (LWP 6047) "python" 0xb774d424 in __kernel_vsyscall ()
6 Thread 0xb40fdb70 (LWP 6048) "python" 0xb774d424 in __kernel_vsyscall ()
5 Thread 0xb04ffb70 (LWP 6078) "python" 0xb774d424 in __kernel_vsyscall ()
4 Thread 0xafcfeb70 (LWP 6079) "python" 0x08068184 in
instancemethod_dealloc (im=0x9e25f2c)
at Objects/classobject.c:2359
3 Thread 0xaf4fdb70 (LWP 6080) "python" 0xb774d424 in __kernel_vsyscall ()
2 Thread 0xaeaffb70 (LWP 6086) "python" 0xb774d424 in __kernel_vsyscall ()
* 1 Thread 0xb759c6c0 (LWP 5983) "python" 0xb774d424 in __kernel_vsyscall ()
【分析】
原因就是违反了UNIX上程序设计的准则之一:多线程程序里不准使用fork
在不使用多线程的情况下,Python语言的并发模型就剩下多进程或者进程+协程两种模式了。
其实是各有利弊了,多进程进程间通信对大量的简单的异步处理逻辑来说太麻烦;协程需要自己准确控制协程之间协作式的调度。
有没有可能还是使用多线程而又不带来fork时可能死锁的问题呢?答案是肯定的。
死锁的本质是在fork子进程时,父进程的子线程获取的锁不能在子进程中得到释放,当子进程要用到该锁时,就会被永远阻塞。
死锁不是毕现的原因就在于通常情况下,父进程fork子进程时,父进程的子线程没有没被释放的锁。
怎么把小概率事件也杜绝呢,我们要做的是就是把这种“遗传”阻断在父进程创建任何子线程之前。
【解决】
调整下初始化进程池的时机,使得在fork子进程时,主进程当时没有任何子线程存在。
高亮的进程号即为fork出来用来产生进程池进程的进程,进程号的大小即反映了创建进程的先后。
调整前,高亮的进程晚于主进程下的各线程,调整后则早于主进程下的各线程。
调整前:
python(22843)─┬─python(22859)─┬─python(22860)
├─python(22862)
└─{python}(22861)
├─{python}(22855)
├─{python}(22856)
├─{python}(22857)
├─{python}(22858)
├─{python}(22863)
├─{python}(22864)
├─{python}(22865)
└─{python}(22866)
调整后:
python(11225)─┬─python(11239)─┬─python(11240)
├─python(11242)
└─{python}(11241)
├─{python}(11243)
├─{python}(11244)
├─{python}(11245)
├─{python}(11246)
├─{python}(11247)
├─{python}(11248)
├─{python}(11249)
└─{python}(11250)
【总结】
1. Python中尽量少用线程
2. 如果非要用,要注意尽量不要再fork子进程
3. 如果前面两条非要违反,请让子进程先于子线程创建