python进程池停止_Python2.7:进程池

I. 项目中使用python进程池遇到的问题

废话不多说,实际在用python开发过程中,有一个逻辑需要多进程任务共享数据,因此采用进程池+Manager.dict()方式。项目上线后发现了一个致命的问题:某几个进程在启动或者运行中途,突然不打日志了。更头疼的时,每次停止运行的进程还不相同。

最开始大家把死锁住的进程的实现代码review了一遍,没有发现问题;接下来,问题发生时尝试用gdb排查,但是都是c栈调用,完全没有头绪。后来求助Google和知乎,发现了一篇知乎文章也是类似问题:一个关于Python的死锁问题。

回顾工程代码的设计,列出可能导致上述问题:竞争锁时导致某几个进程死锁了。这个问题归结于python的logging虽然是线程安全的,但不是进程安全的。因此设计了一个logging agent,专门设计了一个带锁的Queue,其他进程在调用LOGGER静态方法时,实际是往Queue中放打印日志内容,而logging agent只需要一直不断从Queue中取数据打印到日志文件中。

实际cup数量是4个,但是在进程池中塞了7个进程数量,可能导致Process Pool处理不过来。另外有一种说法,是python2.7的Process Pool自带bug。

程序中触发了某个意外的错误,导致进程崩溃退出。*(这个概率应该不大,因为不可能每次都是不同的进程意外崩溃;此外,更多情况是在程序启动的时候就‘崩溃’了?)*

其他未知原因。

II. 痛定思痛,决定重构试试

重构的原因当然不止上述原因,anyway,已经走上了重构的道路。重构后改进如下:取消logging agent,将日志分开打印;

业务不相关的进程分离,最多的一个进程池放了6个进程;

更改部分业务逻辑和代码架构;

其他(不重要,如更改了命名等);

重构后版本在测试中倒是不再出现进程卡主的情况了,但是在我捣鼓中发现了下述问题:

1. 程序中设置了大小为N的进程池,实际ps -ef | grep ${process_name}发现实际数量总比预期的多2个。

2. kill进程组中某个子进程后,再ps -ef | grep ${process_name}发现进程数量没有减少,而kill掉的进程号确实改变了,观察该进程对应的日志,已经停止输出了。

3.kill多出来的某个进程之后,程序报错,在日志中打印“Broken Pipe”错误。

作为一个程序员,当然对上述发现非常好奇,尤其是**多出来的进程究竟是什么?**

III. 寻求Pool源码

网上查了好些时候,没有发现相关的说明。连python官网上也没有对此说明,于是只能寻求python源码。

1. Pool的初始化函数__init__

代码中中文备注都是笔者加的,如有错误之处,欢迎斧正。

def __init__(self, processes=None, initializer=None, initargs=(),

maxtasksperchild=None):

self._setup_queues()

# _setup_queues()函数初始化:

# 1. self._inqueue和self._outqueue为SimpleQueue()

# 2. self._quick_put = self._inqueue._writer.send

# 3. self._quick_get = self._outqueue._reader.recv

#

self._taskqueue = Queue.Queue()

self._cache = {}

self._state = RUN

self._maxtasksperchild = maxtasksperchild

self._initializer = initializer

self._initargs = initargs

打断补充几点个人的理解:

1). self._inqueue和self._outqueue的作用:Pool和worker进程的通信管道,管道内容是task,其实就是通过apply,apply_async等方法传递进来的用户真正要运行的函数。

2). self._taskqueue的作用是普通队列,存储task。

3). _cache的功能还没有理解充分,只知道它保存了一个tupe,记录任务执行成功与否和任务执行结果。

4). _state有三种状态,分别为:RUN = 0

CLOSE = 1

TERMINATE = 2

5). _maxtasksperchild可以设置任务执行次数,默认None,表示任务执行无限次。

if processes is None:

try:

processes = cpu_count()

except NotImplementedError:

processes = 1

if processes < 1:

raise ValueError("Number of processes must be at least 1")

if initializer is not None and not hasattr(initializer, '__call__'):

raise TypeError('initializer must be a callable')

self._processes = processes

# process - 进程池大小,默认是cpu个数或者1。

#

self._pool = []

self._repopulate_pool()

# _repopulate_pool(): 往self._pool加满worker进程

#

继续补充:

1). 经常有网友问,Pool的默认个数是多少,相信这段代码应该非常清晰地告诉了答案:cpu个数或者1。

2). _pool是一个list,上限数量是_processes,存储内容是worker对象。

3). _repopulate_pool()函数可以说是非常吊诡,他的作用就是把_pool填满。但是,后面的代码中_handle_workers方法循环检查_pool中的进程,发现有退出的就清除,然后再调用这个方法,装一个不实际干活的worker进来。(Python2为了把_pool填满,也是操碎了心啊!殊不知,我之前一直用进程数量来确保程序没挂,真是坑!)

self._worker_handler = threading.Thread(

target=Pool._handle_workers,

args=(self, )

)

self._worker_handler.daemon = True

self._worker_handler._state = RUN

self._worker_handler.start()

# _handle_workers线程:守护进程,旨在清理已退出的worker,并启动新的worker替代。

#

self._task_handler = threading.Thread(

target=Pool._handle_tasks,

args=(self._taskqueue, self._quick_put, self._outqueue,

self._pool, self._cache)

)

self._task_handler.daemon = True

self._task_handler._state = RUN

self._task_handler.start()

self._result_handler = threading.Thread(

target=Pool._handle_results,

args=(self._outqueue, self._quick_get, self._cache)

)

self._result_handler.daemon = True

self._result_handler._state = RUN

self._result_handler.start()

self._terminate = Finalize(

self, self._terminate_pool,

args=(self._taskqueue, self._inqueue, self._outqueue, self._pool,

self._worker_handler, self._task_handler,

self._result_handler, self._cache),

exitpriority=15

)

初始化函数在这里end了。最后补充说明:

1). _handle_workers功能已经说过了,不再赘述。不过一定要强调一点的是:在python2.7中,用进程池的话,千万不要相信ps -ef | grep ${process_name}!!!因为只要Pool还在的话,它一定会call出新的无用的worker做花瓶。(捂脸)

2). _handle_tasks功能就是从self._taskqueue中取task,塞到self._inqueue中,让worker取出执行;如果放task失败,则会往_cache放入(False,e),程序片段如下:

try:

put(task)

except Exception as e:

job, ind = task[:2]

try:

cache[job]._set(ind, (False, e))

except KeyError:

pass

3). _handle_results功能就是从self._outqueue取出worker执行完的结果,执行callback(如果有的话)。

4). Finalize不是很明白,但是顾名思义,应该是跟回收进程相关的。

2. 代码实践

了解了源码之后再次验证,写了一个非常简短的demo,命名为pool_test.py:

import multiprocessing

def ProcessWorker(_id):

count = 0

while True:

print "worker %d : %s~" % (_id, count)

count = count + 1

def main():

woker_num = 3

pool = multiprocessing.Pool(processes= woker_num)

for i in range(3):

pool.apply_async(ProcessWorker, (1, ))

pool.close()

pool.join()

if __name__ == '__main__':

main()

运行程序后用ps -ef | grep pool_test查看如下:

Pool的大小明明就设置了3,结果ps出来的进程数是5。

继续查看pid=9140的进程,发现有四个子线程:

开始作了,尝试把pid=9145进程kill掉,再用ps -ef | grep pool_test查看如下:

哎,进程数没有改变!只不过没有9145的进程了,多了一个10594进程;再查看另一个tty,发现没有“worker 2”的输出了,只剩下0和1。可见,10594就是已被杀死的9145的“替身”。

尝试把pid=9140杀死,看看会出现什么:

哈,这下子只剩下真正在干活的两个进程了。替身进程和9140已经不在了。

最后,从表征上来看,9140和9142的作用似乎一样,只要其中一个进程被杀死,另一个进程也会随之死亡。而且用top -H -p 9142查看,也是4个线程。

于是问题来了,9140和9142究竟有什么区别?

留个坑给自己吧,后面慢慢再接着研究。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值