Py学习  »  Python

数据科学 |详解如何用Multiprocessing进行并行计算(附Python代码)

运筹OR帷幄 • 3 年前 • 359 次点击  
↑↑↑↑↑点击上方蓝色字关注我们!






『运筹OR帷幄』原创


作者:周岩


编者按

借着人工智能时代的东风,Python成为了目前发展最快的编程语言社区。虽然Python上手简单,使用灵活,但是使用Python进行科学计算的运行效率一直被人们诟病。好在现在的多核CPU成为主流,对于一些计算密集型的任务,我们可以采用多进程的方式对其进行并行加速。本篇文章就来详细介绍一下multiprocessing的原理,以及使用中的一些问题。


自从人工智能时代的到来,Python作为主流编程语言已经成为了在科学计算领域除MATLAB外的首选语言。Python有着强大的社区支持,完整的生态和部署方便,上手简单等特点,使得使用Python进行计算任务的研究者越来越多,很多传统的计算领域也逐渐向Python转变。


虽然在GPU计算方面Python因为其“胶水”语言的特性首先融合的基于C++开发的几大深度学习框架并获得更大的发展,但是基于CPU计算的需求也在逐步增加。但是Python对于计算密集型的任务并不具备很高的效率,这也是其一直饱受诟病的原因之一。不过也不是完全没有解决的办法,在多核CPU的时代,并行化是提高计算效率的主流,在Python编程中,对计算任务合理加入并行化可以极大的提供计算效率。


Python的并行化模块主要有multiprocessing和multithreading两种。multithreading由于只能利用单核计算,但是变量共享方便,因此multithreading主要用于高并发的I/O密集型任务。而对于计算密集型任务,真正能发挥多核CPU实力的是multiprocessing,原因是由于Python的GIL的限制。


GIL:又名全局解释器锁,python在设计当初(那个年代哪来的多核,电脑都没来普及),也是python的一个设计思想,为了数据安全,怎么个为数据安全考虑呢(一个进程里面只有一个GIL锁),即同一时间只能一个事件发生,我们来捋一下请求过程,第一个线程过来拿到任务,向python申请GIL锁,拿到锁以后条用os的原生线程,然后再调用CPU,python2这个时候有个问题,为了实现多线程并行效果(cpu的上下文切换),它有一个ticke计数,只要达到100后释放,或者这个时候有IO操作,就会切换到下一个线程。
但是这里我有个问题,如果就是同时开两个线程的话,就是两个线程的同时切换,如果我开了5个线程,第一个线程切换的时候,其它4个线程要申请GIL锁才能执行操作,但是这个GIL锁给谁呢?这个时候就会出现一个竞争,会消耗资源(别忘了,python通一个时间值有一下事件再执行),这个就是python在多核CPU上的利用率并没有那么优秀。[8,9]


下面这篇文献介绍的更加详细:

为什么Python多线程无法利用多核


01

如何使用Multiprocessing?



 1.1 常用的类和函数:



1.2 一些例子

对于这两个类的使用,笔者比较推荐使用pool类中的map方法,因为可以自动分配进程执行任务。但是map方法比较适合批量处理的任务,并且自动进行子进程控制,对于需要手动控制子进程的任务来说,用apply方法更加适合。对于Process类来说,会适合更加精细的线程控制情况。这里找到一些例子可以用来体会其中的用法。


02

深入理解Multiprocessing的一些原理

此部分转载自博客[4-7],详细阅读请点开文末的参考链接,在此感谢原博客作者的贡献。



2.1 跨进程对象共享

在mp库当中,跨进程对象共享有三种方式,第一种仅适用于原生机器类型,即python.ctypes当中的类型,这种在mp库的文档当中称为shared memory方式,即通过共享内存共享对象;另外一种称之为server process,即有一个服务器进程负责维护所有的对象,而其他进程连接到该进程,通过代理对象操作服务器进程当中的对象;最后一种在mp文档当中没有单独提出,但是在其中多次提到,而且是mp库当中最重要的一种共享方式,称为inheritance,即继承,对象在父进程当中创建,然后在父进程是通过multiprocessing.Process创建子进程之后,子进程自动继承了父进程当中的对象,并且子进程对这些对象的操作都是反映到了同一个对象。


这三者共享方式各有特色,在这里进行一些简单的比较。



2.2 关于Queue

Queue是mp库当中用来提供多进程对象交换的方式。对象交换和上一部分当中提到的对象共享都是使多个进程访问同一个对象的方式,两者的区别就是,对象共享是多个进程访问同一个对象,对象交换则是将对象从一个进程传输的另一个进程。


multiprocessing当中的Queue使用方式和Python内置的threading.Queue对象很像,它支持一个put操作,将对象放入Queue,也支持一个get操作,将对象从Queue当中读出。和threading.Queue不同的是,mp.Queue默认不支持join()和task_done操作,这两个支持需要使用mp.JoinableQueue对象。


由于Queue对象负责进程之间的对象传输,因此第一个问题就是如何在两个进程之间共享这个Queue对象本身。在上一部分所言的三种共享方式当中,Queue对象只能使用继承(inheritance)的方式共享。这是因为Queue本身基于unix的Pipe对象实现,而Pipe对象的共享需要通过继承。因此,在一个典型的应用实现模型当中,应该是父进程创建Queue,然后创建子进程共享该Queue,由父进程和子进程分别读写。例如下面的这个例子:

另一种实现方式是父进程创建Queue,创建多个子进程,有的子进程读Queue,有的子进程写Queue,例如:

由于使用继承的方式共享Queue,因此代码当中并没有明显的传输Queue对象本身的代码,看起来似乎只要将multiprocessing当中的对象换成threading当中的对象,程序仍然能够工作。反之,拿到一个现有的多线程程序,是不是将threading改成multiprocessing就可以工作呢?也许可以,但是更可能的情况是你会遇到很多问题。


03

Multiprocessing的跨进程参数共享

然multiprocessing很好用,但是由于进程锁GIL的存在,使得在一些复杂任务中,对于参数的传递就不是很方便了。至于其中的原因,我们来慢慢解释。


首先,对于一个multiprocessing的程序来说,是有一个主进程(我们用M表示)来启动其他子进程(用S_i来表示)的。


那么同一套代码,相同的变量,是如何从主进程M分配到子进行S_i中的呢?这就要提到程序入口和python的包导入机制。正如上面程序中常见的语句if __name__ =='__main__': ,这段代码的含义是当该程序做为主程序M启动时,执行下面的代码。对于multiprocessing的程序来说,这段代码是必不可少的,因为它提供了主进程入口,并通过主程序来启动子程序。如果没有这段代码,python无法明确判断那个是主进程,这样就无法分配子进程。了解了主进程的入口,那么我们就该聊一聊进程间变量的关系以及代码执行顺序的问题, 如下图:

从图中可以看到子进程和主进程之间的区别,其中主进程和子进程分别初始化了if __name__ =='__main__':之前变量。


但是注意,这些变量和函数在没有改变的情况下,是具有相同的id的,当各自的进程中修改这些变量后,会在各自的进程中生成一个自己的id。这个机制是由python特性决定的,有点类似copy函数。具体的实验可以参考我之前的这篇知乎:Python 多进程之进程调用和执行顺序。

链接:https://zhuanlan.zhihu.com/p/76584615


言归正传,有了前面这层机制的铺垫,我们就来聊聊mutiprocessing中参数共享的问题。通常来说,进程间参数共享要通过一个可以跨进程通信的变量,这里比较常用的应该属于Queue。这个用法类似队列的先入先出,典型的应用就是生产者和消费者模型。


但是,Queue也不是所有的变量都可以共享。关键因素在于理解multiprocessing对于进程间变量的传递机制。


multiprocessing的变量在进程间传递,无论是通过map、apply等函数,还是通过Queue来共享,都是默认需要将对象序列化后进行传递(python一切皆对象)。这就有一个关键的问题,python的序列化方法多数基于pickle,而multiprocessing也不例外。


pickle并不是所以对象都可以序列化,它的限制为:

对于复杂对象,如weakref这样的对象是不支持序列化的。


那么如何对包含复杂对象参数的问题进行并行化呢?我们就要用到上面的机制。


首先,我们要将包含复杂对象的参数进行尽可能的分离,分成可序列化的部分和不可序列化的部分。其中不可序列化的部分最好是在子进程运行过程中不会被改变的,这样就不会有额外的内存开销。


然后,将这部分不可序列的部分定义在程序的开头,这样就可以作为参数读入到各个子进程中。


其次,将可序列化的部分作为参数传递给map函数,然后在子进程中将这两部分参数结合,作为一个完整的参数运行程序。


接下来我们给出一个例子:

这个例子中,Brian2生成的net包含weakref对象,无法直接被作为参数传递。但是net的状态state是一组np.array对象,可以被pickle。因此这里在函数前面先生成net,再通过net自带的store和restore函数传入state来解决问题。


可以注意到,这里的state是通过queue来传递的,另外,multiprocessing中Queue和Manager().Queue不是一样的机制。Manager().Queue是通过代理模式进行对象的共享,而Queue是通过继承的方式,这种方式在windows里面无法使用。


04

Multiprocessing的进程执行顺序

上一次写到了python多进程的方法的应用,但是后续应用的过程中发现多进进程程在应用过程中的一些进程调用和执行的小技巧,那么我们用代码来看一下具体现象。

可以看到在我们一共用进程池中创建了3个进程,他们的id分别是13772,8272,16740,加上主进程1536,我们一共运行了4个进程。并且在`add`函数中也是可以看到顺序的执行着这几个进程。最后我们执行完分进程后,再次回到主进程再次运行了add函数。


不过值得注意的是,在执行分进程函数前, print("start",os.getpid()) 是在 add 函数外的一段代码,应该只在主进程中执行,为什么还会打印出分进程的id呢?


答案就是在我们在用multiprocess时要在最后的主进程入口加入 if __name__ =='__main__':这段代码,在这段代码之前的代码都不是作为主程序入口的,因此在创建主进程的时候所以的文件都被加载一遍,因此才会打印出分进程的id。

如果有需要只在主进程中执行代码,一定要安排在主程序的代码入口以后。


另外还有一个现象,cpu在调用进程的时候是根据进程是否空闲来决定的,在用pool来创建进程池时,是否创建新的进程是根据当时cpu的空闲进程数决定的,比如我们去掉add函数中的time.sleep(5)得到的结果就是这样的:

可以看到虽然在创建了几个子进程,但是由于程序执行的速度特别快,所以实际上只有一个进程被调用。


而当Pool中的进程数量少于任务时,比如这里设置traies=3, 但是只申请2个进程p=Pool(2),可以看到的现象是函数体外的代码只被用了2次。由此可以知道,每个子进程都是在初始化时同主进程一样加载一下函数体外的代码,但是一旦建立好进程,那么多个进程是统过循环的方式对函数传入参数来进行计算,并最后返回结果。

可以看到在存在sleep函数时,进程是2个一组运行的,这两个运行完成后,下一次循环才开始(当任务不够分配的时候是默认按照进程顺序分配的) 。


以上这些问题都是在windows进行的测试,由于windows和linux底层对于multiprocessing的实现不同,所以表现也不相同。具体说来,Linux是通过fork方式来将主进程的变量直接copy一份过来用,而windows不能用fork方法,因此是直接在子进程重新运行一遍程序,相当于import。所以上面的例子会出现如下的结果:

可以看到主进程中 print("start",os.getpid()) 只执行了一次,子进程并没有重复执行命令。


具体可以参考:

python多进程,multiprocessing和fork_qm5132的博客-CSDN博客



参考文献

[1]封面图来源:https://realpython.com/async-io-python/

[2]https://zhuanlan.zhihu.com/p/279944908

[3]https://zhuanlan.zhihu.com/p/76584615

[4]Python multiprocessing库使用手记(引子)

[5]Python multiprocessing 使用手记[1] – 进程模型

[6]Python multiprocessing 使用手记[2] – 跨进程对象共享

[7]Python multiprocessing 使用手记[3] – 关于Queue

[8]https://www.cnblogs.com/lei0213/p/8004269.html

[9]https://zh.wikipedia.org/wiki/%E5%85%A8%E5%B1%80%E8%A7%A3%E9%87%8A%E5%99%A8%E9%94%81

[10]为什么Python多线程无法利用多核

[11]python并发函数apply_async踩坑记录


欢迎社会各界加入『运筹OR帷幄』算法知识星球!

随着算法相关专业热度的提升,考研读博、留学申请、求职的难度也在相应飙升,『运筹OR帷幄』建立了【算法社区】知识星球,涵盖运筹学、数据科学、人工智能、管理科学、工业工程等相关专业,集结社区60W+专业受众的力量,提供给大家一个共同的学习交流平台,结交志同道合的伙伴。

# 加入知识星球,您将收获以下福利 #

依托『运筹OR帷幄』60w+专业受众和60+细分领域硕博微信群的算法技术交流

与国内外Top名校教授|博士和名企研发高管一起交流算法相关技术干货

海量学界|业界(独家内推)招聘|实习机会发布,申请|求职面试经验交流

数学模型|算法|论文|学习资料分享与提问,倡导同行交流,寻找志同道合的“队友”

每月开展一次“人气话题”和“人气回答”评选,百元红包奖励分享和互动

每月一次“领读人”带队Paper|教学视频|原创技术推文等线上Meetup小组学习

享受『运筹OR帷幄』各大城市线下Meetup免费入场资格,拓展人脉



相关文章推荐

运筹OR帷幄“pandas实战教程”电子书正式上线啦!如今的互联网时代,数据成为了一座巨大的金矿。掌握数据的获取、处理和分析已经成为了各个行业的必备的基本技能。Pandas作为最为强大成熟的数据处理工具之一,已经被用在机器学习和统计分析领域的方方面面。如何快速学习掌握Pandas的使用技巧,《“pandas实战教程”电子书》用开源免费的方式带你从数据分析小白进阶为Pandas能手。

点击蓝字标题,即可阅读《数据科学|”pandas教程“电子书正式上线啦》



本文福利

可以在 公众号后台 回复关键词:“ pandas电子书 获取pandas学习教程,如果觉得有用, 请勿吝啬你的留言和赞哦!


—— 完 ——




文章须知

文章作者:周岩

责任编辑:周岩

审核编辑:阿春

微信编辑:玖蓁

本文由『运筹OR帷幄』原创发布

如需转载请在公众号后台获取转载须知





Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/103875
 
359 次点击