美文网首页
Python GIL 全局解释器锁

Python GIL 全局解释器锁

作者: vickeex | 来源:发表于2020-02-18 19:28 被阅读0次

有所收获,特将原文翻译如下。

原文:What is the Python Global Interpreter Lock(GIL)?

URL: https://realpython.com/python-gil/

目录

  • GIL为Python解决了什么问题

  • 为什么选择GIL作为解决方案

  • 给Python多线程应用带来的影响

  • 为什么GIL至今未被移除

  • 为什么未从Python3中移除

  • 如何解决Python GIL带来的问题


简而言之,Python全局解释器锁(Global Interpreter Lock, GIL)是一个互斥信号量(互斥锁),保证仅有一个线程能持有Python解释器的控制权。

意味着任何时刻都只有一个线程可以处于执行状态。GIL不会对执行单线程应用的开发者带来影响,但可能会成为计算密集型(CPU bound)和多线程型应用的性能瓶颈。

即使在拥有多CPU内核的多线程架构中,GIL一次也只允许执行一个线程,被认为是Python臭名昭著的特性。

阅读本文,你能了解到GIL为什么会影响Python程序的性能,以及如何减轻由此带来的对代码的影响。

GIL为Python解决了什么问题

Python使用引用计数(reference counting)来进行内存管理。这意味着Python里创建对象会持有一个引用计数变量,用于追溯指向该对象的引用的数量。当引用计数的值减为零时,该对象占有的内存会被释放。

来看一个简短的代码示例,演示了引用计数的工作原理:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

在示例中,指向空列表对象[]的引用计数为3,3个引用分别是:a, b, 传递给sys.getrefcount()的参数。

回到GIL:

问题在于引用计数变量需要被保护,以防在竞争情况下的两个线程同时增加或减少它的值。如果这种情况发生了,可能会引起从未释放的内存泄露,更坏的可能是在仍存在对象的引用时错误地释放内存。这些可能导致Python程序的崩溃或其他不知所以然的bug。

为跨线程共享的所有数据结构加锁,可以保证引用计数变量的安全,防止不一致的修改。

但为一个或者一组对象加锁,意味着多个锁将同时存在,可能会引起死锁(Deadlocks,只在拥有多个锁时才可能发生)。另一副作用是重复地获取和释放锁会带来性能下降。

GIL是作用于解释器自身的唯一的锁,并新定一条规则:执行任何Python字节码都需要获取解释器锁。由此防止死锁(现在就只有一个锁啦),也不会引入过多额外的性能开销。但是它强制所有计算密集型的Python应用成为了单线程应用。

GIL也用于其他语言,比如Ruby,但它并不是这类问题的唯一解决方案。为了避免在线程安全的内存管理中使用GIL,有些语言采用引用计数以外的方法,比如垃圾回收。

另一方面,这些语言也通常必须在其他地方提升性能(比如使用JIT编译器),以弥补GIL带来的单线程性能损失。

为什么选择GIL作为解决方案

那为什么要在Python中使用这么一种看上去很碍事的方式呢?Python的开发者是否做了一个错误的决定?

嗯...用Larry Hastings的话来说,GIL的设计是让Python广受欢迎的原因之一。

操作系统没有线程概念时,Python就已经存在了。当时Python被设计为易于使用以加快开发速度,越来越多的开发者也开始使用它。

很多现有的C库也在那时进行扩展,Python需要他们的特性。为了防止不一致的更改,这些C扩展需要GIL提供的线程安全的内存管理。

GIL的实现很简单,也很容易添加到Python中。由于只需要管理一个锁,它提升了单线程应用的性能。

非线程安全的C库也变得容易集成。这些C扩展也是Python容易被不同社区采用的原因之一。

如你所见,GIL,是在Python的历史早期 CPython开发人员所面临的一个难题的务实的解决方案。

给Python多线程应用带来的影响

当你在阅读典型的Python程序(或任何与此相关的计算机程序)时,CPU密集型和I/O密集型应用的性能差异往往很大。

CPU密集型应用指CPU占用率极高的应用,包括数学计算(比如矩阵乘法),搜索,图像处理等等应用。

I/O密集型应用是指将大量时间花费等待在I/O读写的应用,I/O可以来自于用户、文件、数据库、网络等。I/O源在准备I/O数据前可能会进行其他操作,导致I/O密集型应用有时在获取对应数据前会等待大量时间。比如,用户可能会思考很久到底输入什么,数据库也可能正在执行其他的查询操作。

来看一个简单的CPU密集型应用,它执行的是倒数计时:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

我的四核电脑上跑这段代码获取的输出如下:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

现在稍许改动代码,让它用两个并行线程来进行倒计时:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

运行结果如下:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

若你所见,两个版本花费的时间几乎相同。在多线程版本里,GIL阻碍了CPU密集型线程的并行执行。

GIL对I/O密集型的多线程应用影响不大,因为线程在I/O等待时共享了这个锁。

但是完全CPU密集型的应用(比如使用多线程处理图像的应用)不仅会由于锁的存在变成单线程,执行时间也会相对单线程编写的代码程序有所增加(比如上面这个栗子)。这种增加是锁的获取和释放带来的开销。

为什么GIL至今未被移除

Python开发人员收到很多对GIL的怨言,但对于一门像Python这样被广泛应用的语言而言,在不引起向后不兼容问题的情况下,删除GIL这种操作实在是太大的改动了。

GIL当然可以被移除,开发人员和研究人员也尝试过多次,但无一例外地破坏了现有的C扩展,这些扩展在很大程度上都依赖于GIL提供的解决方案。

当然哈,对于GIL解决的问题也有其他的解决方案,但是其中某些降低了单线程和多线程I/O应用的性能,而另外某些真的太难了。毕竟,没人愿意新版本发布后 现有的Python程序跑得更慢,是吧?

Python的创建者和“仁慈的独裁者”(Benevolent Dictator for Life,BDFL)Guido van Rossum,在2007年9月的文章中给了社区一个答案:删除GIL并不容易。

只有当单线程应用(以及多线程的I/O应用)的性能不会降低时,我在愿意在Py3k中发布一系列补丁。

但至那以后的所有尝试都没有满足这个条件。

为什么未从Python3中移除

Python3确实有机会从头开始启用一些功能,并在此过程中破坏一些现有的C扩展,这些扩展需要更新并移植到Python3中才能使用。这也是Python3早期版本被社区接纳较慢的原因。

但为什么不将GIL一起移除呢?

移除GIL会让Python3的单线程性能比Python2更慢,能想到会带来什么结果。确实无法质疑GIL带来的单线程优势,所以最后结果是Python3 仍然留下了GIL。

但是Python3也对现有的GIL进行了重大的改进。

我们讨论了GIL对“仅计算密集(only CPU bound)”和“仅I/O密集(only I/O bound)”的多线程应用带来的影响,但对于有些线程是I/O型、有些线程是CPU型的应用呢?

众所周知(嘤嘤嘤,我不知道吖),对于这类应用,Python的GIL会导致I/O线程饥饿,因为这类线程不能从CPU型线程中获取到GIL。

因为Python内置了一种机制,以强制线程在一段固定的连续使用后释放GIL,如果没有其他获取到GIL,则该现场继续使用GIL。

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

这种机制的问题在于,多数情况下,CPU型现场会在其他线程可以获取GIL之前重新获取到GIL。结论由David Beazley得出,也可以找到相关的可视化解释。

[ 我的理解:线程会在一个时间段后释放GIL,但在释放的时间点,I/O仍可能处于等待中而不去获取GIL,于是CPU线程则会继续持有该GIL,如此导致IO线程饥饿。 ]

如何解决Python GIL带来的问题

如果Python GIL给你带来了困扰,可以尝试以下几种解决方案。

多进程 vs 多线程(multi-processing vs multi-threading)

最常用的方式是使用多进程而非多线程。每个Python进程都有自己的解释器和内存空间,所以GIL就不成问题了。

Python的multiprocessing模块能让我们轻松地创建进程,如下:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

运行结果如下:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

与多线程相比,性能有所提高。

但时间没有降低到一半,因为进程管理也带来了其他开销。多进程比多线程开销大,所以这可能会成为扩展的瓶颈。

备选的Python解释器

Python实现了多种解释器,最受欢迎的是CPython,Jython,IronPython,PyPy,分别用C,Java,C#和Python实现。GIL只在原始的Python实现(Cpython)中存在。如果你的程序和相关库有其他可用的实现,不妨尝试一下。

多等一会会儿

(不是在卖萌。)许多Python用户都利用了GIL带来的单线程优势。多线程码农也不用为此烦恼,因为Python社区中最聪明的一些人正在努力从CPython中移除GIL,“Gilectomy”是其中较闻名的尝试之一。

Python GIL经常被认为是一个神秘且具难度的话题。但是请记住,作为一个Python高手(Pythonista),通常只有在编写C扩展或者在程序中使用CPU型多线程时才会被影响。

在这种情况下,本文已为您提供了解GIL及如何处理它的相关内容。如果你想了解GIL底层工作原理,我建议您观看David Beazley的Python GIL演讲。

相关文章

  • Python 并发总结

    0x01 GIL锁 C语言写的Python解释器存在全局解释器锁GIL(Global Interpreter Lo...

  • python多线程

    python基础之多线程锁机制 GIL(全局解释器锁) GIL并不是Python的特性,它是在实现Python解析...

  • python中的GIL详解

    原文链接 GIL并不是Python的特性,Python完全可以不依赖于GIL。 ​ GIL 锁: 全局解释器锁,作...

  • Python的GIL

    Python的GIL是什么# GIL即全局解释器锁(Global Interpreter Lock) 首先我认为,...

  • Python day28_GIL 深拷贝浅拷贝

    GIL(全局解释器锁) GIL面试题如下 描述Python GIL的概念, 以及它对python多线程的影响?编写...

  • GIL

    一,GIL的概念 python全局解释器锁。 二,GIL产生的原因 GIL 并不是 Python 语言的特性,它是...

  • 008 12. python提高-1

    12.1. GIL GIL(全局解释器锁) GIL面试题如下 描述Python GIL的概念, 以及它对pytho...

  • python GIL 详解

    GIL介绍 python全局解释器锁(global interpreter lock, GIL)限制了任何时候只能...

  • Python 经验 - 多线程与多进程

    多线程 GIL GIL(Global Interpreter Lock)即全局解释器锁。 在Python中一个线程...

  • GIL和多进程,多线程

    1、GIL是什么?GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python...

网友评论

      本文标题:Python GIL 全局解释器锁

      本文链接:https://www.haomeiwen.com/subject/qornfhtx.html