鹅厂分享会丨关于python多线程的思考

导语在知乎等地方经常看到有人问,Python的多线程是不是鸡肋?为何我用多线程性能一点没有提升,有时候性能反而下降?在这里通过日常工作中遇到的问题以及自己的一些总结,来一探Python多线程究竟是不是鸡肋;如果不是,那又该如何使用。

1、遇到的问题

工作中常用到python来分析文件,统计数据;随着业务的发展,原先的代码性能受到了一定的挑战,下面根据两个案例来讲解在python的使用过程中,遇到的一些问题,以及自己的一些总结。

案例一:数据统计,将文件按照一定的逻辑统计汇总后,入库到本地db中。

最开始的代码流程框图:

《鹅厂分享会丨关于python多线程的思考》

大概流程:

1、循环读文件,按照一定格式将文本进行拆分计算

2、根据指定的key来统一汇总数据

3、入库本地DB,入库时,会先查找db中是否存在这条记录,然后再判断是否插入db中;相当于这里会有两次db操作,一次查询,一次写入。

一开始业务量小,db数据量少,整个流程耗时较短,在秒级能够完成,随着业务发展,所需时间也有秒级变成了分钟级,十分钟级别等。

测试1:

通过对其中的一个业务某天的数据进行测试,它的耗时主要分部为,读文件(文件大小1G左右)耗时2秒,逻辑计算及汇总58s,数据入库32s;总耗时大概在1分半钟。

1.1 方案1

流水线形式的多线程

《鹅厂分享会丨关于python多线程的思考》

线程1负责读取数据,然后通过python自带的Qeueue,Q1传递数据给线程2;

线程2负责逻辑计算,然后通过Q2传递数据给线程3;

线程3负责汇总数据,然后通过Q3传递数据给线程4;

线程4则入库数据;

这个方案在实现之后立马就被废弃了,它的效率比单进程的效率低很多,通过查看系统调用之后,发现是因为多个线程一直在竞争锁,以及线程切换导致其执行效率还不如单线程。

1.2 方案二

数据分片+分段多线程

在不同的时机采用多线程来处理,同时尽量避免多线程对同一资源进行竞争,以减少锁的切换带来的消耗。因此这里在逻辑计算和数据入库阶段分别采用数据分组,多线程执行的方式来进行处理。

分段一、逻辑计算和汇总,将内存读到内存中后,按照线程数量,将数据切分成多块,让第i个线程thread[i]处理第i份数据data[i],最后再将计算得到的4份数据汇总,按照相同的key进行汇总;得到sum_data

分段二、将sum_data按照线程数量,切分成多份,同样让thread[i]处理sum_data[i]的数据,让他们各自进行数据的查询以及写入操作。

《鹅厂分享会丨关于python多线程的思考》

测试2:

对同一个文件进行测试,读文件耗时2秒,逻辑计算及汇总62s,数据入库10s;总耗时大概在70多秒,相比最开始的单线程,时间大概下降了20多s;但可以看出,逻辑计算的时间相比单线程确增加了不少,而入库操作的时间减少了20多s;这里就引发一个问题,逻辑计算跟入库的差异在哪里?为何前面的多线程性能下降,而后面性能确大幅度提高。

这里的原因究竟为何?

案例二:案例2的整体流程为,将几份不同的数据源从db中取出来,按天取出,经过一定的整合后,汇总插入到一个目标db中。

一开始的代码流程:

《鹅厂分享会丨关于python多线程的思考》

同样最初的时候,需要整合的数据量较少,db中的数据量也较少。随着业务增长,每天需要处理的数据量也逐渐增加,并且db中的数据量也越来越大,处理的时间也从开始的秒级别也逐渐增加到分钟级别,每次都是统一处理一个月的数据,整体耗时需要几个小时。

测试3:

对某天的数据进行测试,结果为:取数据+整合 耗时30s;插入数据耗时约8分钟

更改成以下模型:

入库操作同样需要先根据key查找当前db中是否存在该条数据,不存在则写入

《鹅厂分享会丨关于python多线程的思考》

测试4:

取数据+整合 耗时30s;插入数据耗时约2分钟

更改之后性能大幅度提升,由原先的8分半钟,缩减为不到2分半钟左右,缩减的时间主要体现在入库阶段;

从以上两个例子可以看到,当涉及I/0操作时,python的多线程能发挥较好的性能;而当涉及到CPU密集型逻辑运算时,python的多线程性能不升反降。这里都是由于python的GIL在发挥的作用。

2、了解python的GIL

这里我们使用的解释器为官方实现的CPython,它是由C语言实现的python解释器。同时还存在由Java实现的Jython解释器,由.NET实现的IronPython等解释器。这里我们主要是依据CPython来讲解GIL锁。

GIL,全称Global Interpreter Lock, 是一个全局解释器锁,当解释器需要执行python代码时,都必须要获得这把锁,然后才能执行。当解释器在遇到到I/O操作时会释放这把锁。但如果当程序为CPU密集型时,解释器会主动的每间隔100次操作就释放这把GIL锁,这样别的线程有机会执行,具体的间隔次数是由sys.setcheckinterval( number )来设定这个值,通过sys.getcheckinterval()返回这个值,默认为100。所以,尽管Python的线程库直接封装操作系统的原生线程,但Python进程,在同一时间只会有一个获得了GIL的线程在跑,其它的线程都处于等待状态等着GIL的释放。就这样对于CPU密集型操作来说,多线程不但不会提升性能,还会因为线程切换,锁竞争等导致性能的下降。

在我们上面的两个例子中,当涉及到数据的查询与插入时,都需要进行I/O交互,并且会等待数据库服务器返回,这个时候,线程会主动释放锁,其他线程能够合理利用这个时间,来做同样的事情。

知道了GIL之后,我们才能更加合理的使用python的多线程,并不是所有场景都适用于多线程。

同样,Python的多线程也并不是大家所说的鸡肋,在适合的场景用上了,还是能够起到惊艳的作用

-END-

内容转自公众号:腾讯课堂coding学院

    原文作者:腾讯课堂
    原文地址: https://www.jianshu.com/p/58d27b605133
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞