本篇文章主要了解一下Python 中的GIL问题。
众所周知,在使用到python的多线程时,我们都会遇到GIL这个问题,使得多线程性能陷入瓶颈。
什么时候会遇到GIL问题?
说到GIL,很多人会把其称之为python多线程中必然会遇到的一个问题。
其实不然,这不是python本身的问题,而是我们常用的一个解析器cpython所带来的问题。
而cpython只是python的一种解析器,意味着还有许多其他的解析器是没有这样一个问题的。
但cpython作为最常用的一个解析器,在一定程度上受到了广大编程爱好者的青睐。
换言之,在使用python进行多线程编程时,若使用cpython解析器必然需要遇到GIL问题。
到底什么是GIL?
GIL 全称 Global Interpreter Lock(全局解释器锁)。
简单来说,GIL是一个互斥锁,它只允许一个线程来控制Python解释器。
这意味着在任何时间点只有一个线程可以处于执行状态,
这将可能成为CPU-bound(计算密集型)多线程代码中的性能瓶颈。
而且,即使是在多核CPU的情况下也是如此,所以GIL问题可以说是臭名昭著了。
GIL对python有什么用?
Python使用“引用计数”进行内存管理。
这意味着在Python中创建的对象具有“引用计数”变量,
该变量用于跟踪指向对象的引用数。
当此计数达到零时,释放对象占用的内存。问题在于,当两个线程同时更改这个值时,其需要受到保护。
如果没有保护好它,可能会导致内存泄露(有些内存可能永远无法释放);
更糟糕的情况比如:当对象依然存在而内存却不正常地提前释放。
这可能会导致Python程序中出现崩溃或其他错误。通过向跨线程共享的所有数据结构添加锁,
可以保持此“引用计数”变量的安全,从而不会导致以上这种现象。给每个对象或者对象组加锁,意味着可能产生多个锁,
就可能产生另一个问题——死锁问题(只可能发生在有多个锁的情况)。
重复地获取和释放锁将会在另一个程度上降低性能。GIL是解释器本身的单个锁,它增加了一条规则,即执行任何Python字节码都需要获取解释器锁。
这可以防止死锁(因为只有一个锁)并且不会引入太多的性能开销。但它使任何受CPU限制的Python程序都是单线程的。GIL虽然也被解释器用于其他语言(如Ruby),但并不是解决此问题的唯一方法。
有些语言通过使用除引用计数之外的方法(例如垃圾收集)来避免GIL对线程安全内存管理的要求。
为什么选择GIL来解决问题
那么,为什么这样的一个方法还在python中被使用?这个方法真的这么糟糕吗?
- 事实上,GIL的设计是python变得如此受欢迎的原因之一。
在python起初被使用时,操作系统还没有线程的概念。
python被设计来使用开发人员可以更快更简单地编程,所以越来越多的开发人员开始使用它。python中有许多现有的C库扩展。为了防止不一致的更改,这些C扩展需要GIL提供的线程安全内存管理。
GIL实现简单且容易整合进python。由于只需要管理单个锁,它使得python中单线程的程序性能得到提升。
有了GIL,非线程安全的C库变得更容易集成。而这些C扩展,也成为不同社区采用python的原因之一。
综上所诉,GIL确实是一个实用的解决方法,可以解决CPython开发人员在Python初期遇到的一个难题。
GIL对python多线程编程的影响
- 对于CPU-bound(计算密集型) 和I/O bound(I/O密集型)两种不同类型的程序,在性能上有所不同。
-CPU密集型的程序使得CPU利用达到极致,这包括进行数学计算的程序,如矩阵乘法,搜索,图像处理等。
I/O密集型程序需要花费时间等待输入\输出,其可以来自用户、文件、数据库、网络等。
其有时必须等待大量时间,直到它们从源获得所需内容。
例如,用户需要考虑在实际进程中在输入框中输入什么,或者考虑从数据库中查询什么等。由于GIL阻止了CPU密集型线程的并发执行,几乎同样的程序,
采用单线程和多线程的时间,往往多线程的时间并不会比单线程的短甚至更长。由于在等待I/O时,GIL可以在不同线程中流转,故GIL对I/O密集型多线程程序影响并不大。
为啥GIL问题仍未被移除?
python的开发人员对此有很多怨言,但若是移除GIL,则可能会导致向后不兼容问题。
GIL显然可以被删除,开发人员和研究人员过去已经多次这样做了,
但所有这些尝试都破坏了现有的C扩展,这些扩展在很大程度上依赖于GIL提供的解决方案当然,也有很多别的方法可以替代GIL,但其中有一些会降低单线程以及多线程I/O密集型程序,还有一些难以实现。
毕竟,我们不希望移除GIL后得到的新版本,比旧版本性能差吧。
为什么Python3中仍然没有将它移除?
Python3 确实有机会从头开始启动很多新的功能,并打破一些现有的C扩展。
但将原有的程序更新并移植到Python3仍需要时间,这也是为啥一开始Python 3在社区中并不那么受欢迎。如果移除GIL,Python3在单线程程序上的性能将低于Python2 ,
你可以说python单线程程序的性能多亏了有GIL,所以这就是为什么Python3中仍然有GIL问题。但,在Python3中,确实针对GIL问题,做了很大的提升:
我们前面讨论了GIL对于CPU密集型以及I/O密集型程序的影响,
那么当程序既是CPU密集型又是I/O密集型程序呢?此时,GIL问题将会使得I/O密集型线程饿死(无法从CPU密集型线程那获得GIL)。
这是因为Python的内部机制决定了GIL在固定的连续使用时间后将会被释放,
但如果此时没有程序获取GIL,那么原来那个线程将会继续运行。
而在这个机制中,CPU密集型线程将会在其他线程获取GIL前重新获取GIL。这个问题在2009年由Antoine Pitrou在Python 3.2中得到修复。
他添加了一种机制,可以查看被删除的其他线程的GIL获取请求数,
并且在其他线程有机会运行之前不允许当前线程重新获取GIL。
那到底如何解决GIL问题呢?
如果GIL给你带了麻烦,可以尝试以下几种方法:
Multi-processing vs multi-threading:最受欢迎的一个方法是使用多进程代替多线程。
每一个Python程序有自己的Python解释器和存储空间,所以将不会有GIL问题。
Python中有一个multiprocessing 模块 。与多线程相比,多进程在性能上有了不错的提升,
但时间并没有随着进程的增加而减少相应的倍数,因为进程管理有自己的开销。
进程比线程的开销更大,因此这将成为多进程程序运行的瓶颈。选择其他Python解释器:Python有很多解释器比如最受欢迎的几种:Cpython、Jython,
IronPython还有Pypy,分别是由C、Java、C#和python实现的。GIL仅存在于Cpython中。
如果你的程序有这些包,你也可以尝试用这些解释器。再等等吧:很多Python使用者享用着GIL所带来的单线程程序的性能,而多线程程序的开发者也不必担心。
Python社区中有一群大佬正在致力于将GIL从CPython中移除。
其中,有一种尝试被称为Gilectomy,不过其实好像最近没啥动静。