python多进程多核利用心得体验

帅比萌擦擦* 提交于 2019-12-05 20:37:29

        总结一下之前的项目,主要用到了python多进程的知识,其他的一些零碎的辅助知识也会用到,这里主要对整体框架进行总结,至于性能,因为经验问题,不能优化的很好,加上本项目有很多文件的读写,只能算稳定而已。

        这个项目是大量的音频文件格式和频率转换,大概300多万个,一个转成7个,原来那个也要用,也就是说最后大概有300*8W个文件,总共大概2T的数据。好了,回忆下要求,首先转换过程中不能破坏原始文件的目录结构;第二,保证所有文件都转换过;第三,异常检测,日志分析,进度检测;第四,保证意外死机等情况下,重新跑代码可以继续执行上次中断的位置而不是要重复在进行转换(重复转换会出现1个文件本来转成8个了,那重复执行,这8个又会各自再生出8个,就产生错误了);最后,分析汇总错误日志,得到最终处理结果,问题文件汇总查看原因。

        其实说白了,就是保证不出现不可逆的错误,保证稳定执行,保证准确,保证完整,保证效率。那么我们要有一个解决问题的思路,一开始说是,有好几台机器可用,就用分布式框架吧,例如hadoop。。可惜时间短任务重,从头学的话,不得至少一个月?还是Python来的快,多进程执行,多核利用,有几个核就开几个进程,标准库multiprocessing完全可以胜任,一开始想的是thread多线程,后来想到Python的多线程不是类似java的多线程,Python有一个全局锁机制,多个线程其实并不是并行执行(是不是并发更准确?),而是轮换执行,况且不能有效利用多核,那就确定最终的路线是:multiprocessing+多核(我用的进程池Pool,因为自己一个一个造的进程容易出错,而且性能也没有Pool好),具体的代码后面再说。

         好了,下面就说代码吧,首先要从给定的目录中找到目标文件作为待处理任务列表,我写的函数如下:

<span style="font-size:14px;">def get_filepath(src):

        filepath = collections.deque()

        for root,dirs,files in os.walk(src):
                for afile in files:
                        if "_" not in afile:
                                filepath.append(os.path.join(root,afile))
        return filepath
</span>

      os.walk()可以得到一个三元tupple(dirpath, dirnames, filenames),其中第一个为起始路径,第二个为起始路径下的文件夹,第三个是起始路径下的文件。其中dirpath是一个string,代表目录的路径,dirnames是一个list,包含了dirpath下所有子目录的名字。filenames是一个list,包含了非目录文件的名字。由于转换之后的文件名带有"_",所有代码里的if可以防止突发进程停止等需要重跑代码的情况,可以避免重复转换。那么接下来就是要处理这个任务列表了,可以看到这个任务列表我用的deque队列来存储,其实我本来也用的列表list,但是后面有些性能问题(比如两端的插入删除还是deque更适合),我还是用的deque队列,对这个队列进行过滤,过滤掉非amr文件(好吧,我们处理的amr文件转换),代码如下:

all_filepath = get_filepath(src)
for line in all_filepath:
        ret = os.path.splitext(line)[-1]
        if (ret == '.amr'):
                work_filepath.append(line)
      为了保证代码不会被意外操作所停止,我们还注册了信号量,不过这个实现的很粗糙,没有往下细细研究:

signal.signal(signal.SIGINT, my_exits)
signal.signal(signal.SIGTERM, my_exits)
signal.signal(signal.SIGTSTP, my_exits)
      my_exits是自己实现的函数,就是打印收到了什么信息,并不退出程序,这里就不贴了,下面贴出Pool的使用代码:

        pool_size = multiprocessing.cpu_count()
        pool = multiprocessing.Pool(processes=pool_size,initializer=start_process,)

        #pool_outputs = pool.map_async(amr2amr_worker.convert_rm_file,work_filepath)

        Sum_work_deque = 'total_numbers:%s'%len(work_filepath)
        total_log_file = FILE+'/log/total_numbers.log'
        with open(total_log_file,'w') as f:
                f.write(str(Sum_work_deque))

        while len(work_filepath)>0:
                try:
                        file = work_filepath.popleft()
                        print file
                        result = pool.apply_async(amr2amr_worker.convert_rm_file,(file,))
                except:
                        break
        pool.close()
        pool.join()

      其中,amr2amr_worker是工作模块,convert_rm_file是入口函数,包括具体的转换、检测完整性、日志记录、文件转换前后的对比等操作。我们机器是24核心(有超线程的,不是全物理核),开24个进程,这样大体的流程就出来了,pool.apply_async()可以保证执行时非阻塞的,如果没有async那就是阻塞版本了,效率就会低。

      后面的代码没啥好贴的了,只是有的情况是自己没考虑完全的,在自己测试的时候没发现问题,真跑的时候有很多小缺陷,例如,文件转换完后,总数不对,却没打出日志,在执行结束后,发现文件数量在某台机器上高的离谱,后来发现其实是这台机器的小文件(小于10k的大约一百万个)数量超多,搞得我还以为这是重复转换了。

      当然这都不是最重要的教训,最重要的是在做这个东西的时候,偷懒,用了别人的一个写的一个模块,也没仔细推敲,直接用了,而且在测试的时候,非常稳定。。。直到真正跑的时候,你会发现真正的数据会奇形怪状,竟然有0KB文件的存在。。。刚好在调用别人模块时崩了,而我又没在那个模块加日志和异常控制,查了大半天才发现有0kB文件的存在。所以,在写代码的时候,除了实现正常流程之外,各种异常情况一定要考虑到,这就是经验积累的开始了。

      再说个日志分析的吧,这个处理的难度取决于你当时日志的可读性,如果你直接一个字符串毫无格式可言,那以后分析日志,简直是惨,下面贴出我的日志:

time:Mon Jul  6 16:14:44 2015 ok_count:1 source_count:1 dest_count:8
time:Mon Jul  6 16:14:51 2015 ok_count:2 source_count:2 dest_count:16
time:Mon Jul  6 16:14:44 2015 ok_count:1 source_count:1 dest_count:8
time:Mon Jul  6 16:14:45 2015 ok_count:1 source_count:1 dest_count:8
time:Mon Jul  6 16:14:44 2015 ok_count:1 source_count:1 dest_count:8
time:Mon Jul  6 16:14:52 2015 ok_count:2 source_count:2 dest_count:16
time:Mon Jul  6 16:14:51 2015 ok_count:2 source_count:2 dest_count:16
time:Mon Jul  6 16:14:51 2015 ok_count:2 source_count:2 dest_count:16

      这个八个进程各自打印的进度信息(当然这个正确的日志,错误的格式也都一样),用空格隔开是为了万一出现数量或其他等问题,可以直接分析这个日志,无非就是读文件,字符串的处理,很简单,容易看懂的,下面贴出自己分析这个日志的代码(这是临时加的,没正规写):

#!/usr/bin/env python
import glob

log_list = glob.glob("*.log")
ok_count = 0
dest_count = 0
source_count = 0

if len(log_list) != 0:
        log_list.remove("total_numbers.log")
        for file in log_list:
                with open(file,'r') as f:
                        word_list = f.readline().split(' ')
                        dest_str = (word_list[-1].strip())
                        dest_number = int((dest_str).split(':')[-1].strip())
                        dest_count += dest_number
                        source_str = (word_list[-2].strip())
                        source_number = int(str(source_str).split(':')[-1].strip())
                        source_count += source_number
                        ok_str = (word_list[-3].strip())
                        ok_number = int(str(ok_str).split(':')[-1].strip())
                        ok_count += ok_number
        print "dest_count:%s"% dest_count
        source_count_new = source_count*8
        print "source_count:%s"% source_count_new
        print "source_count:%s"% source_count
        print "ok_count:%s"% ok_count

        with open('total_numbers.log') as f:
                print "total_numbers:%s"% int(f.read().split(':')[-1].strip())
else:
        print "no log file,please check others!!!"
      当时只为了快速出结果,可以看到,我连函数都没写。。直接把思路用python写出来了,我想这也是python的魅力吧,反正我用着挺爽,如果让我用C写的话,写着写着发现。。。咦,我当时咋想的来着?哈哈,开个玩笑,自己的C一写就烦。。不知道为何,但还得练,以后还有用的。。纠结中努力吧!加油!



易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!