9

Python 的 GIL

 3 years ago
source link: https://xie.infoq.cn/article/fc17122a3bc232eb5af203497
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

引言

原文链接: 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的时候,回收机制就会回收这个对象,释放对象占用的内存空间。看一个简单的例子:

>>>importsys
>>>a =4902384823
>>>b = a
>>>sys.getrefcount(a)
3

通过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的,最简单的,就是下边这个程序,他可能会跑满你某个核心

a =0
while1:
a +=1

你的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,那将是从里到外的巨大改进,前后兼容又成了巨大的问题。但是改进肯定有的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK