进程被kill原因_Python2.7:进程池

d098c3fbca76b4a8dc3d70ae12c1cab6.png

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

废话不多说,实际在用python开发过程中,有一个逻辑需要多进程任务共享数据,因此采用进程池+Manager.dict()方式。项目上线后发现了一个致命的问题:

  1. 某几个进程在启动或者运行中途,突然不打日志了。更头疼的时,每次停止运行的进程还不相同。

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

回顾工程代码的设计,列出可能导致上述问题:

  1. 竞争锁时导致某几个进程死锁了。这个问题归结于python的logging虽然是线程安全的,但不是进程安全的。因此设计了一个logging agent,专门设计了一个带锁的Queue,其他进程在调用LOGGER静态方法时,实际是往Queue中放打印日志内容,而logging agent只需要一直不断从Queue中取数据打印到日志文件中。
  2. 实际cup数量是4个,但是在进程池中塞了7个进程数量,可能导致Process Pool处理不过来。另外有一种说法,是python2.7的Process Pool自带bug。
  3. 程序中触发了某个意外的错误,导致进程崩溃退出。*(这个概率应该不大,因为不可能每次都是不同的进程意外崩溃;此外,更多情况是在程序启动的时候就‘崩溃’了?)*
  4. 其他未知原因。

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

重构的原因当然不止上述原因,anyway,已经走上了重构的道路。重构后改进如下:

  1. 取消logging agent,将日志分开打印;
  2. 业务不相关的进程分离,最多的一个进程池放了6个进程;
  3. 更改部分业务逻辑和代码架构;
  4. 其他(不重要,如更改了命名等);

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

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查看如下:

722ef54be624a901ff4e70ebc70062cd.png

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

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

6c4a864e24ccb0e8197f8997a1d97ecd.png

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

e0e52f0c7ba9f8cc57bde786491238df.png

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

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

20b0edd8620558d7e6ca3258e3a2ee3a.png

哈,这下子只剩下真正在干活的两个进程了。替身进程和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、付费专栏及课程。

余额充值