Python 的 GIL
引言
原文链接:https://yunsonbai.top/2020/08/27/GIL/
聊一个老生长谈的问题: Python 的 GIL。
结合日常工作的总结和前人的经验,聊一聊我自己对此 GIL 的理解。
希望本文对 Python 的初学者有一些帮助。
到底什么是 GIL?
GIL 全称: Global Interpreter Lock。注意这里有个 Interpreter(Cython、Jython),说白了这把锁其实是加在解释器上的,这把锁只允许一个 Python 线程获得解释器控制权,简单说就是某一时刻只能有一个线程运行(单线程)。对 GIL 褒贬不一,对于单线程程序来讲其实没有什么影响,但是对于多线程程序,有的时候就会成为影响性能的瓶颈点。对 GIL 的形容,你可能在网上还会找到类似臭名昭著的形容词,其实我觉着万事都不能这么绝对,它存在既有存在的原因和用处。
GIL 对 Python 到底有什么帮助?
我们知道 Python 使用了计数的方式来管理对象的回收,进而管理内存。简单来说就是每个对象都会有个计数,当这个计数变为 0 的时候,回收机制就会回收这个对象,释放对象占用的内存空间。看一个简单的例子:
通过 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 的,最简单的,就是下边这个程序,他可能会跑满你某个核心
你的 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,那将是从里到外的巨大改进,前后兼容又成了巨大的问题。但是改进肯定有的。
版权声明: 本文为 InfoQ 作者【yunson】的原创文章。
原文链接:【http://xie.infoq.cn/article/fc17122a3bc232eb5af203497】。文章转载请联系作者。
评论