前言
本篇文章主要是看完UnderstandingGIL.pdf后的一些理解
http://www.dabeaz.com/python/UnderstandingGIL.pdf
GIL什么是?
简单翻译一下:在CPython解释器下,全局解释器锁GIL是为了保证python多线程安全的一把互斥锁。这把锁是必要的,主要因为CPython的内存管理器不是线程安全的。(但是,自从GIL诞生后,所有其他功能都是基于GIL来实现了)
划重点:
1、GIL是一把在Cpython解释器下,针对多线程的全局互斥锁。
解读:Cpython解释器下的GIL,也就是说有些python解释器是不使用GIL的(但Cpython是主流)。
GIL是针对多线程的锁,所以跟多进程,协程什么的就不搭边啦。
2、这把锁是必要的。
解读:python的线程是真正的操作系统线程,python将线程的管理完全交由操作系统。(python认为操作系统自身的线程管理已经很好了,没必要再搞一套出来。感觉就像我们平时用标准库一样)这种设计的合理性在于当我们的CPU是单核时,GIL问题是不存在的,一个线程释放GIL时,任何一个线程被唤醒(也包括自身)都能获取到GIL,继续工作,等下一次再释放GIL时,依然是所有线程来获取GIL,如下图。但多核CPU时,现象是有所差别的,后面会继续分析。
3、其他功能都是基于GIL来实现
解读:这就解释了为啥这么多年过去了,在多核CPU盛行的情况下,GIL这种明显的性能瓶颈为什么依然去不掉,因为要改的实在太多了
GIL是如何工作的
关于python单核单进程,单核多进程,单核多线程,多核多进程,多核多线程的性能比较,本文就不再一一贴图了。结论上来讲,python的多核多线程要比单核单线程的性能还要差!
在了解GIL工作原理之前先提两个不太熟悉的概念:
1、Tick,可以理解为程序执行时的字节码(可能理解有误,没有找到官方的说明,需要用到dis.dis方法将代码转换成汇编后才能识别出1个tick)。一个简单的减法操作会用到4个tick。
2、OS Scheduing,操作系统在多任务操作时,会将待进行的任务写入的一个队列,并分配优先级。
下面要分情况讨论了(触发GIL释放有两种情况:I/O触发和ticks触发,这里不做划分,只分析单核和多核):
1、单核CPU情况
当线程1达到释放GIL的条件时,向操作系统发送释放GIL的信号(signal),操作系统拿到GIL后,把GIL分配给schedule队列准备好的线程2上,线程2拿到GIL后,进行上下文切换,开始工作。并且GIL有很大的几率依然会回到线程1,这样很好,减少了不必要的上下文切换。
2、多核CPU情况:
当线程1达到释放GIL的条件时,向操作系统发送释放GIL的信号(signal),操作系统拿到GIL后,把GIL分配给schedule队列,把此时和单核CPU出现差别。在多核下,可运行的线程们在每个CPU上都可以被唤醒,每个可以被唤醒的线程都认为自己可以去争抢这个GIL,但最终只有1个CPU上的线程能够抢到这个GIL,而其他被唤醒的线程发现没有GIL了,又接着回去干自己的工作,并且浪费了CPU的时间。
多核CPU多线程的解决方案
1、根据我们对定义的解读,既然GIL是Cpython解释器下的GIL,那么我们寻找一个不用GIL的python解释器就好了。(虽然理论上没毛病,但没见过这么做的)
2、等。等python版本更新,然后用新版本的python。没错,我们什么都不用做,python社区的大神们比我们更想解决这个问题。目前来看,从python3.2之后,引入了TIMEOUT来替换ticks的计数,这会使性能更好一些。
3、根据之前的分析,我们能够确定单核CPU的时候,是不存在GIL问题的,而GIL又是针对某个进程下的多线程。由此可以提出一种解决方案:把进程绑定到固定的CPU上,然后在这个进程下使用多线程处理业务。这时候,虽然计算性能比较依赖单个CPU的主频,但绕过了GIL,并发能力绝对是提高了。
介绍一个叫做affinity的包,将进程绑定到指定CPU用,之前写过一段小例子可供参考https://github.com/dsgdtc/understanding_bindingcpu
4、虽然说了GIL这么多的坏话,但并不意味着完全舍弃多线程了。
参考资料
http://www.dabeaz.com/python/UnderstandingGIL.pdf
网友评论