Python GIL 全局解释器锁

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

原文: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演讲。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352