python多进程/进程池/进程间共享数据实用场景分析和实践踩坑记录

我们都知道,python这门语言比C++慢主要有两个原因,一是python是动态类型语言,需要边解释边执行,二是全局解释其锁(GIL)的存在,让python无法利用多核CPU并发执行。

对于第一点是语言本身决定的,没啥好办法,第二点还是可以有些应对措施的,比如:

1、多线程 threading 机制依然是有用的,用于IO密集型计算。因为在 I/O (read,write,send,recv,etc.)期间,线程会释放GIL,实现CPU和IO的并行,因此多线程用于IO密集型计算依然可以大幅提升速度,但是多线程用于CPU密集型计算时,只会更加拖慢速度

2、使用multiprocessing 的多进程机制实现并行计算、利用多核CPU优势。为了应对GIL的问题,Python提供了multiprocessing
python提速的方法博主最近遇到一个场景,目的是对稠密深度估计模型的推理结果与点云真值进行比较,计算评测结果,主要包括以下两步:

第一步需要读取数据,数据包括RGB图片数据和三维点云真值,而且由于数据量较大,读取速度较慢(一对数据接近一秒,一共有1000对左右)

第二步需要在全部读取完数据后,把所有的图片和点云数据一一对应,分别存储在两个列表里,进行像素级的准确度评估,由于图像分辨率较高,计算量较大

由于博主做了多组实验,需要大量重复以上过程,所以脚本运行效率就非常重要。如何提高效率且得到准确的结果呢,现进行如下分析:

第一步主要是I/O操作,属于I/O密集型计算,第二部主要是数值计算,属于CPU密集型计算,由前文可知,多线程可以实现I/O和CPU的并行,但是在我这个场景下,必须全部读取数据后才可以计算,所以无法使用I/O和CPU的并行,只能使用多进程方式,而且由于第二步是CPU密集型计算,多进程只会使速度更慢,所以采取第一步多进程,第二步单进程的方式。

最容易想到的多进程代码示例:(pred_depths与gt_depths分别代表推理结果和真值列表,data_read()函数执行数据读取,8个进程依次往pred_depths与gt_depths两个列表里append数据)

from multiprocessing import Process

pred_depths = []
gt_depths = []
processes = []
process_num = 8

for idx in range(process_num):
    t = Process(target=data_read, args=(idx, pred_disps, gt_depths)
    t.start()
    processes.append(t)

for t in processes:
    t.join()

但这段代码却犯了两个错误,首先来看第一个。

第一,直接跑这段代码,会发现跑完多进程后,pred_depths和gt_depths两个list仍然是空列表,并没有执行数据读取,原因是python多进程机制中,进程之间是相互独立的执行单元,彼此间直接共享数据不可行。要共享数据必须使用multiprocess提供的共享内存接口,如multiprocessing下的Value,Array,Queue,如果要共享 list,dict,可以使用强大的 Manager 模块。

实践中就会发现,这里的Array只能是一维列表,而且必须初始化,例如:

a = multiprocessing.Array('i', [1, 2, 3])

我这里需要初始化为空列表,而且不止一维,所以需要使用list:

pred_depths = multiprocessing.Manager().list()
gt_depths = multiprocessing.Manager().list()

上面代码改为:

import multiprocessing as mp

pred_depths = mp.Manager().list()
gt_depths = mp.Manager().list()
processes = []
process_num = 8

for idx in range(process_num):
    t = mp.Process(target=data_read, args=(idx, pred_disps, gt_depths)
    t.start()
    processes.append(t)

for t in processes:
    t.join()

第二,改到这里程序似乎没问题了,但后面的计算结果却和之前的单进程不一致,仔细分析后发现,之前单进程是这样往pred_depths和gt_depths两个列表存储数据的:

for .....:
    pred_depths.append(pred_depths)
    gt_depths.append(gt_depth)

在多进程中,由于多个进程同时往两个列表中存储数据,无法保证两个列表中数据的顺序是一一对应的,所以可能造成后面计算的错误。为解决这个问题,我们采用字典结构:

pred_depths = mp.Manager().dict()
gt_depths = mp.Manager().dict()

在data_read函数中,用相同的索引确保能找到同一组数据:

for idx in range(.....):
   pred_depths[idx] = pred_depth
   gt_depths[idx] = gt_depth

在后面计算的地方,也使用索引去访问数据和真值,就可以一一对应上了,结果和单进程一致。

到这里是不是大功告成了呢?发现问题又来了,得到pred_depths和gt_depths两个字典后,进行后面的单进程计算时却发现计算显著变慢了!!比没用多进程的时候慢了好多倍,结果整体时间好像没有缩短,这不白忙活吗?

问题出在哪里呢?这里想到开头提过,CPU密集型计算假如使用多进程/多线程,速度反而会变慢,但在这里我们似乎多进程已经停止了,都已经join了,为什么仍然变慢了?

我们来用top命令看一下CPU使用情况。和单进程对比就会发现,单进程执行中一直只有一个python进程,而多进程在数据读取中有9个进程,在计算时仍有3个进程,看来多进程并没有停止,然后加了terminate等命令也没有效果。

观察现在程序和单进程还有哪些区别呢?发现区别在于pred_depths和gt_depths两个字典变量上。单进程中,它们只是普通的dict(),而多进程中我使用了mp.Manager().dict(),用type()函数看一下类型:

<class 'multiprocessing.managers.DictProxy'>

果然不是普通的dict(),有这种代理类型的存在,后面程序仍然是多进程的,所以最后我们需要用dict()强制转一下数据类型:

pred_depths = dict(pred_depths)
gt_depths = dict(gt_depths)

然后再进行后面的计算,终于大功告成:数据读取和计算都变快了!!

示例代码修改如下:

import multiprocessing as mp

pred_depths = mp.Manager().dict()
gt_depths = mp.Manager().dict()
processes = []
process_num = 8

for idx in range(process_num):
    t = mp.Process(target=data_read, args=(idx, pred_disps, gt_depths)
    t.start()
    processes.append(t)

for t in processes:
    t.join()

pred_depths = dict(pred_depths)
gt_depths = dict(gt_depths)

最后,我们再尝试用进程池(multiprocessing.Pool)再改造一下代码,可以变得更简洁:

pool = multiprocessing.Pool(processes=process_num)
for idx in range(process_num):
    pool.apply_async(data_read,(idx, pred_disps, gt_depths))
pool.close()
pool.join()

还要提醒一点:无论是pool.applyasync()还是Process(),里面的目标函数只能写名称,函数的参数在后面用元组表示,如果目标函数写成函数本身,如data_read(),无法实现多进程的哦!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值