python GIL problem

本篇文章主要了解一下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,不过其实好像最近没啥动静。

GIL一直被认为是一个神秘而困难的话题。但作为开发者,如果你正在使用C扩展,或者说你的程序是CPU密集型程序,才会被GIL影响。
分享到