我们在使用python开发的过程中时常听到GIL这个词,并且发现这个词经常和Python无法高效的实现多线程关联在一起,关于python多线程的实现在前面的文章已经介绍过,本文我们主要来了解一下GIL到底是什么?为什么会影响python的多线程。
GIL全称Global Interpreter Lock,官方给出的解释如下:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents
multiple native threads from executing Python bytecodes at once. This lock
is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the
guarantees that it enforces.)
我们知道,和PHP一样,Python也是一门脚本语言,它的特点就是一边解释一边执行。既然不是编译型语言,那么就需要解析器来解析和执行。Python的执行环境,可用的解析器有很多种,例如CPython、JPython、IronPython等等,其中我们最为常用的、也同时是python安装时默认的解析器就是CPython。所以要明确一点,GIL并不是Python本身自带的特性,而是CPython解析器所采用的一种解析方案。
现在的许多编程语言,为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,脚本语言很多都不能支持多线程,例如PHP,但是Python为了利用多核,Python开始支持多线程。而使用多线程随之而来的就是线程间数据一致性和状态同步的问题。我们通常在解决多线程之间数据完整性和状态同步的方法就是给线程加锁,像Java、C++等编译型语言通常都需要程序员自己手动添加和释放线程锁。而Python语言在设计时为了追求简易性和便利性,就设计了GIL这个全局的锁。如官方所说,GIL是一把全局排他锁,而后面很多Python库都严重依赖GIL,导致不能随意修改源码,这便是GIL的来源。
尽管Python完全支持多线程编程, 但是CPython解释器的实现部分在完全并行执行时并不是线程安全的。 实际上,这时候解释器被GIL保护着,它确保任何时候都只有一个Python线程执行。 GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势,甚至就几乎等于Python是个单线程的程序(Python里面只有多进程才能利用到多核,而Java等编译型语言多线程就能利用多核资源)。
在这里有一点要强调的是GIL只会影响到那些严重依赖CPU的程序(比如计算型的)。 如果你的程序大部分只会涉及到I/O,比如网络交互,那么使用Python多线程就很合适, 因为它们大部分时间都在等待,而GIL在程序进入I/O操作等待时是可以释放的。
所以需要注意的是,并不是所有的多线程就能提高程序的运行效率。比如当你使用一个计算密集型的线程去管理图形计算界面,又开了很多别的线程去进行异步I/O操作,那么这个计算密集型的线程会长时间持有GIL,而那些别的进程就只能等待,期间还有线程的创建和管理的性能损耗,总的来说反而性能更差了。
图片来源于网络
为了利用多核资源,又能避免GIL对多线程性能的影响,这里介绍一下Python社区现有的解决方案:
1、使用C扩展
这种方案的主要思想是将计算密集型任务转移给C,跟Python独立,在工作的时候在C代码中释放GIL。这样在计算密集的时候程序执行速度也会提升的很快。例如你要操作数组,那么使用NumPy这样的扩展会非常的高效。但是同样带来不好的问题就是python需要依赖很多C扩展,丧失了一部分简单易用的特性。
2、使用multiprocessing库实现多进程
Python的多线程虽然是单核多线程,但是多进程却是可以真正利用多核资源的,多个Python进程有各自独立的GIL锁,互不影响。
为了弥补thread库因为GIL而低效的缺陷,出现了multiprocessing库。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。当一个线程想要执行CPU密集型工作时,会将任务发给进程池。 然后进程池会在另外一个进程中启动一个单独的Python解释器来工作,这时候原本的线程相当于执行I/O操作,当线程等待I/O结果的时候会释放GIL。
这种方式也有缺陷,当混合使用线程和进程池的时候会增加程序实现时线程间数据通讯和同步的困难。所以最好在程序启动时,创建任何线程之前先创建一个单例的进程池。 然后线程使用同样的进程池来进行它们的计算密集型工作。
3、使用其他解析器
像JPython等解析器,不需要GIL的帮助。但是由于这类是采用Java或C#等语言用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会,许多C扩展的库无法使用。关于其他解析器,也可以了解下PyPy。
Python GIL其实是历史遗留问题,跟我们常常做算法是要舍弃空间还是舍弃时间是一个道理,是性能与python简便性之间权衡后的产物:
1、具有很多CPU计算密集型的程序,GIL只能让多线程以当个线程的形式运行,效率低下;
2、如果只是I/O密集型的操作,多线程能在一定程度上提升程序效率;
3、使用并行程序时为了避免GIL的影响,可以使用多进程、C扩展等等方式解决。
参考链接:http://cenalulu.github.io/python/gil-in-python/