Python的GIL是什么鬼
学习编程的时候,我们少会涉及到多任务。可是在python中使用多任务经常会提到一个GIL锁,那么GIL到底是做什么的?有什么好处么?
GIL是什么
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。中文译为全局解释器锁。
为什么会有GIL
由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。
Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。
慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。
GIL的影响
接下来,我们使用一个案例来看一下GIL对pyuthon的影响。
上面是我们不在开启多线程情况下的运行时间,接下来我们循环同样的次数,看一下运行多线程运行时间
按理说,我们使用多线程的形式,其实是想让我们的程序运行的效率更高,可是我们可以发现,时间上相差不多,如果当我们的数据更大的时候还有可能会变得更慢,那么为什么呢?
当前GIL设计的缺陷
基于pcode数量的调度方式
GIL作为Cpython中的全局解释器锁,主要作用就是保护线程的安全,然后在同一个线程当中,都会先将自己锁住,阻止其他线程的执行。
为了直观的理解GIL对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间。
由图可见,GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。
那么Python的IO密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示IO线程处于等待。可见,当IO线程收到数据包引起终端切换后,仍然由于一个CPU密集型线程的存在,导致无法获取GIL锁,从而进行无尽的循环等待。
简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
然而,GIL不能保证线程绝对安全。
刚才我们提到了,GIL为了提高线程的安全性,那么我们就不用自己给线程加锁了,反正python也会直接给我们加锁,让我们更安全,然而我们看一个案例:
得到的结果是:
两个线程分别加了100000次,我们想得到的结果是200000,然后我们得到的却远远小于这个值,就是因为,我们的全能局解释器锁不能保证我们的线程安全。
是因为,我们GIL不能容忍一个线程一直占用资源,他会轮流执行python的其他线程,由于咱们的CPU执行速度够快从而达到了一种“伪多线程”的效果。
总结
Python GIL不是他的特性,而是历史遗留的产物,因为最开始的计算机多数都是单核cpu,python的这种机制是无可厚非的,但是随即计算机硬件的发展,我们Python想要解除这种机制却不是那么容易了,因为还有其他的框架或者第三放工具在使用这种机制,如果修改,那么则导致其他无法使用。
那么我们就对此没有办法了么?当然不是,我们可以使用其他解释器,也可以使用其他封装的工具类,比如说numpy模块 ,他们就是c语言写的数据分析的模块,我们可以无缝连接使用。