Python: 多线程还是多进程?

       并发编程一般来说,主要有两个目的:程序对并发效果的需求和提高程序运行效率。本文所讲的内容是针对提高程序运行效率这个话题的,因此对于多线程还是多进程的选择,后面将围绕如何提高程序运行效率展开。

目录

一、任务的执行时间拆分

二、线程、进程和CPU调度

三、全局解释器锁(GIL)

四、python线程和进程的区别

五、python中选择多线程和多进程的判断方法


一、任务的执行时间拆分

       程序是用来完成某个或者某些任务的,而执行一个任务的时间主要由IO和CPU计算两个部分组成,对于一个任务的拆解以及分析可以看笔者写的这篇文章,大致过程可参考下图。

       所以,一个程序所花费的时间主要在和外部的数据交换上以及cpu的计算处理上。那么要提高程序的运行效率,也就是缩短程序的运行时间,就可以分别从这两方面入手。

二、线程、进程和CPU调度

       cpu是中央处理器的英文缩写,其是用来执行计算的地方,对于cpu来说,一般线程是分配cpu资源的最小单位,即系统在给多个任务分配cpu资源时,实际上就是对不同的线程分配cpu资源,而且一般线程也会有一个优先级,优先级越高的越先得到cpu资源,同等优先级下,谁先获取资源取决于操作系统的cpu调度算法;而进程是系统分配内存资源的最小单位,所以不同的进程之间是有独立内存的,而一个进程中的线程则共享内存,没有自己独立的内存。给线程分配cpu资源是取决于操作系统的,操作系统有自己调度的算法,所以对于开发者来说,是没有办法从脚本层面来改变cpu资源的分配的。因此,我们想要提高程序的性能,只能从程序本身出发。

三、全局解释器锁(GIL)

       GIL是Cython解释器的特点,是为了便于内存管理而加入的,由于python解释器不仅仅只有用C语言写的,还有用Java写的Jython,而Jython并没有GIL;所以严格来说,GIL不是python自己的特性,只是由于绝大部分的python自带的解释器都是Cython,因此很多人就认为GIL是python的特性;所以如果想摆脱GIL,可以用Jython,但是其他方面Jython往往效率会更低一些,本文后续将全部基于Cython展开。全局解释器锁顾名思义就是解释器中的一把全局锁,全局的意思就是针对该解释器下所有线程的。可以这样去理解GIL:GIL是加在解释器中的锁,而解释器的作用是编译和解释脚本代码,将其解释为机器可以识别的机器语言,每个线程要被执行,必须先经过解释器解释,而要得到解释器的解释,就需要先获得解释器的这把锁;由于一个解释器只有一把锁,而一把锁在同一时刻只能被一个线程获得,所以这就导致在同一时刻,一个解释器下只能有一个线程在工作,这样就无法利用多核的优势,这就是GIL的局限性所在。

       但是需要注意的是,由于只有当线程代码需要被解释器解释时,才会受GIL的限制,而当IO时,程序是在和外部发生数据交换,所以当IO阻塞时,这个过程仅仅是在等待数据,并没有和解释器发生关系,从而在IO阻塞时,GIL会被自动释放,这样就可以让其他的线程获取而运行,这样对于IO线程来说,其并不会占用GIL,从而不会跟其他线程竞争GIL,所以对于IO线程,GIL对其几乎是没有限制的,除了一点点需要被解释器解释的代码部分(但是由于对于IO线程来说,绝大部分的时间消耗在IO上,所以需要解释器的时间是极少的,甚至可以忽略)。此外,还有对于不需要Cython解释器的代码,也是不受GIL限制的,比如对于一些用C语言写的库,比如numpy,运行这些库的代码时,不需要Cython解释器,从而也是不受GIL限制的,不同线程的这部分代码可以真正并行。

四、python线程和进程的区别

       首先对于线程和进程,我们知道一个进程中的多个线程之间的共享内存的,即线程没有自己的独立内存,但是进程是有自己的独立内存的,即进程之间是相互独立的,进程之间要交流通信需要通过特定的方式,而不能像线程一样共享对象状态来实现交流。由于进程有自己的独立内存,因此当在一个进程中创建子进程的时候,那么子进程是会复制父进程的状态以及全局对象的,这样多进程就会消耗额外的内存,特别是当主进程本身就有比较占用内存的对象时,那么多进程是会相当消耗内存的。对于python下多进程在操作系统层面的创建方式,在unix下,其直接在当前主进程的状态下新建一个分支,并独立运行,而在windows下,会先将子进程对象中的目标函数其参数通过pickle进行序列化,保存在内存中,然后再通过管道把序列化后的对象传输给新的子进程中,进行反序列化并通过导入目标函数和参数所在的模块以重构对象,这一点需要在windows下python多进程编程中着重注意,要清楚pickle机制,不然容易写出bug,具体看参考官方文档以及笔者之前的介绍pickle的文章

       当然,python线程和进程之间最为关注的区别在于,python多线程由于GIL的限制,不能实现真正的并行,但是进程可以实现真正的并行。GIL是全局解释器锁,即在一个进程中,也就是一个python解释器下,同一时刻只能有一个线程在工作,所以对于多核cpu来说,单纯的多线程并不能充分的利用cpu资源,这也往往是在python中利用多进程的动机。由于每个进程是有独立的python解释器的,因此每个进程有自己的全局解释器锁,所以多进程之间不会受GIL的限制,进而可以利用多进程来实现真正的并行,充分利用多核CPU资源。

五、python中选择多线程和多进程的判断方法

       本文选择多线程和多进程的主要标准是如何更好的提升程序的运行效率,根据第一部分,对此可以从IO和CPU两个方面去分析。一个基本的思路是:先分析任务是在哪方面花时间,我们应该从最花时间的那部分去优化和解决,比如,如果任务绝大部分的时间都在IO阻塞上,那么就应该增加单位时间内的IO次数,而不是通过并行来降低极少的那部分cpu时间,因为本来cpu时间就极少,比如10ms,那么就算你把它减少到5ms,也是没有什么意义的,我们的目的是尽量减少那总体任务串行请求阻塞需要的1个小时的时间,比如通过多线程将其减少到5分钟,那么5ms相比于55分钟,前者可以忽略

       如果任务主要是在IO阻塞上花费时间,那么严格来说多线程和多进程都可以,目的是增加并发任务,增加单位时间内的IO次数,从而减少任务的运行时间,特别是在进行网络请求的IO任务,程序效率可以显著提升;但是线程数实际上可以设置很多个,这样可以更加有效的增加IO次数,而对于进程,由于之前提到的,会额外的消耗内存,不宜设置过多的进程数,比如对于python3的标准库concurrent.futures来说,其中进程池设置最大的进程数被限制为61个。更重要的是,IO型任务不涉及解释器,所以并不受GIL的限制(虽然cpu部分还是受限制,但是由于IO型任务的cpu时间很少,可以不用考虑),所以对于IO密集型任务,多线程几乎总是一个更优的选择。

       如果任务主要是在cpu计算上耗费时间,那么最直接的办法自然是增加单位时间内该任务获取到的cpu资源,这个可以通过将任务拆分为多个独立的子任务,然后通过多进程来利用多核cpu并行,从而提高效率。要注意的是,这里有个基本的要求是可以将任务拆分为独立的子任务,如果子任务之间是有依赖的,则多进程往往就无能为力了。利用多进程编程,需要注意文章第四部分提到的多进程注意事项。

       此外,还有一个问题是进程数的设置,一般来说,设置的而进程数等于计算机的cpu个数为佳,因为如果进程数超过cpu个数,个数过多,并不能增加cpu利用率,因为cpu往往已经饱和了,而且过多的进程数会增加cpu上下文转换所额外消耗的时间。有一种例外情况是,当计算机同时持续运行着其他的cpu密集型任务时,那么可以通过进程数的增加来增加本任务对cpu的抢占能力,尽管cpu调度是由操作系统决定的,但是在优先级相同的情况下,一个任务的进程数越多,cpu轮转到该任务的概率往往会越大,从而抢占到的cpu资源也会越多,当然,cpu个数不宜过多,不然相对优势上去了,但是绝对运行时间也提高了,得不偿失。实际情况中,我们普通的PC往往没有持续运行的cpu密集的进程,更多的是一些IO密集进程,也有一些间断性的cpu占用较高的进程,这种情况下,即当计算机没有其他cpu密集型进程跟我们抢占cpu资源时(这其实也是大多数的情形),我们就将进程数设置成cpu数。

       对于CPU密集型任务还可能存在的疑问是:多线程可不可以?可否通过增加线程数提高CPU抢占资源的能力?对于第一个问题,答案是不可以。首先对于单核cpu情况下,多线程没有必要;在多核cpu情况下,由于GIL的限制,一个进程下的多线程始终只有一个cpu在执行,因此无法利用多核cpu资源。对于第二个问题,一种很正常的想法是,如果操作系统是基于线程进行cpu调度的,那么增加该任务的线程数,尽管由于GIL无法让其并行,但是应该是可以提高该任务的cpu资源抢占能力的,然而经过笔者的测试,在创建其他的cpu密集进程的前提下,增加线程数相比于单进程,其并不能减少任务的运行时间,两者的时间几乎一样,这说明增加线程数并没有提升任务的cpu资源抢占能力,而且看起来仿佛是基于进程调度一样,这个也是笔者疑惑的地方,不太清楚操作系统关于python多线程cpu调度的底层设计,可能多线程只是python自身的一种抽象设计,但在操作系统层面,实际上只看到其所在的进程?如果有清楚的读者,欢迎释疑,by the way,笔者测试的平台是windows10,python版本是3.7.3。

       最后是对于IO和CPU都相对密集的任务,这时就需要详细分析代码,对IO bound部分利用多线程,对CPU bound部分利用多进程,也可以在多进程中利用多线程,具体情况具体分析,多线程和多进程相结合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值