引言
聊一个老生长谈的问题: Python的GIL。
结合日常工作的总结和前人的经验,聊一聊我自己对此GIL的理解。
希望本文对Python的初学者有一些帮助。
到底什么是GIL?
GIL全称: Global Interpreter Lock。注意这里有个Interpreter(Cython、Jython),说白了这把锁其实是加在解释器上的,这把锁只允许一个Python线程获得解释器控制权,简单说就是某一时刻只能有一个线程运行(单线程)。对GIL褒贬不一,对于单线程程序来讲其实没有什么影响,但是对于多线程程序,有的时候就会成为影响性能的瓶颈点。对GIL的形容,你可能在网上还会找到类似臭名昭著的形容词,其实我觉着万事都不能这么绝对,它存在既有存在的原因和用处。
GIL对Python到底有什么帮助?
我们知道Python使用了计数的方式来管理对象的回收,进而管理内存。简单来说就是每个对象都会有个计数,当这个计数变为0的时候,回收机制就会回收这个对象,释放对象占用的内存空间。看一个简单的例子:
1 |
>>> import sys |
通过getrefcount可以拿到某个对象的计数,插个小插曲,为什么我这里用这么大的数(4902384823), 为什么不用1?细心的读者可以去网上找一下,Python的小整数池,你就会明白。
我们继续聊GIL,想一下这个场景,如果解释器允许多线程并发执行,都要对这个a做操作,会有什么样的结果产生呢?线程1要把a赋值给c,线程2呢,要把a赋值成别的值(原来的a对象技术就会减小1),线程3也要把a赋值成别的值,线程4更狠,直接释放了a,线程5….,试想一下,当出现这么多的进程要操作同一个对象的时候,情况就复杂了,是不是会导致程序的逻辑异常甚至直接崩溃呢?GIL就派上了用场,在某个时刻只允许一个线程运行,很完美的解决了上述问题。不排除还有其他的垃圾回收机制,比如golang就十分复杂,不得不引入STW机制,来进行垃圾回收,要知道在Python诞生之际,操做系统还没有线程的概念。
有人可能会问,那不能给对象加把锁么?这样不就可以同时运行多个线程了么?那就会需要数量庞大的锁,维护这些锁开销很大,另外很可能会触发死锁(多锁情况下的陷阱)情况。而GIL只需要管理一个锁,能提高单个线程的性能。
由于Python简单易用,目前越来越多的人开始学习和使用Python,这其中GIL起着很大的作用。C库的许多扩展,有的需要在Python中实现其功能,而GIL则提供了线程安全的内存管理,可以防止不一致的更改,GIL对Python的快速发展起着不可估量的作用。
另外GIL只需要管理着一个锁,将变得很简单,这在早期的Python设计开发中,GIL是个结合实际情况而做出合适的选择。
有这个GIL我该怎么提升我的代码性能呢?
要想提高代码性能,首先得先弄清楚你得代码是面向什么场景的。只有结合了实际,才能选出适合的方案来。咱们抛开业务讲,无非就是两种情况,CPU 计算瓶颈约束和 I/O 瓶颈约束。
CPU 密集计算型
你的程序是极其耗CPU的,最简单的,就是下边这个程序,他可能会跑满你某个核心
1 |
a = 0 |
你的CPU会不停的工作,甚至累趴下,这会程序已经没有优化的余地了,别说Python,就是任何语言也得望而止步,就好比是你开的车,你已经把挡挂到最高,把油门踩到底了,你还怎么加速,只有这个速度了。比较典型的还有图片计算、视频计算等等也都是耗CPU的计算。
I/O 型
什么是 I/O 型呢?网络访问、磁盘读写这都是典型的 I/O 操作,比如你访问google,你的I/O开销可就大多了,我想会有人应看到TIMEOUT,即便你访问baidu,嗖的一下打开的网页,那段时间,对于计算机世界来说那也是极为漫长的等待,而这段时间你的CPU几乎提前进入了退休生活,一个字,闲。
磁盘访问也是一种 I/O 操作,磁盘如果是老式机械磁盘,那也是慢的出奇,即便后来的SSD相对于CPU时间片段来讲也漫长的很。
提升思路
-
计算密集型(以下基于单核)
对于CPU密集型来讲,无能为力,如果本身一个CPU 1秒只能计算一个结果,你就是换成什么语言,什么架构它也不可能超过一个,最好的情况是等于1个,但基本达不到,为什么?先说Python,线程维护是有开销的,这个CPU得做,所以怎么可能到1个呢?有的初学者可能考虑换个语言加多线程,那可能比单线程还惨,多线程来回上下文切换,更耗资源
不说硬件的提升,光说代码,这时候应该考虑算法本身的提升,比如图片计算里,考虑换成矩阵计算会不会好些?或者看是不是有些计算是多余等等。当然如果考虑硬件的提升,那就没有止境了。
-
I/O 型(以下基于单核)
对于Python, I/O密集型的应用还是有提升的余地的。如果说I/O等待占据了大部分时间,我们这会可以考虑“多线程”(如果已1秒为单位,即便是GIL存在,那也会有n个线程被执行),如果I/O等待的时间远远大于获取GIL(全局大锁)的时间,那多线程势必会提升速度。多进程,虽然只有一个CPU,还是要看进程切换的时间和I/O等待的时间的对比,如果I/O等待时间很长,那多进程之间的切换开销也就微不足道了。
协程,这个是个好东西,在一个线程里边,协程切换是用户态的,开销小的多,这也是提升性能的利剑。
-
其他
多核情况下,不管计算密集型还是I/O 型,多进程一定是能提升性能,毕竟一个人干活跟10个人干活肯定速度是不一样的。
GIL依然存在
那能不能去掉GIL呢?答案显然是可以的,不然golang怎么做的?java怎么做的?可是为什么不去掉GIL呢,我认为还是历史原因,首先很多C库是基于GIL做的,推翻了重整,你懂得,很难。另外如果真的完全去掉GIL,那将是从里到外的巨大改进,前后兼容又成了巨大的问题。但是改进肯定有的。