python为什么有GIL

原文:https://blog.nowcoder.net/n/f6999765005349b3b47ab8e8921dc898

GIL详解

GIL全称global interpreter lock,全局解释器锁,是 Python 解释器中的一个布尔值,受到互斥保护。这个锁被 CPython 中的核心字节码用来评估循环,并调节用来执行语句的当前线程。

每个线程在执行的时候都需要先获取GIL,保证同一时刻只有一个线程可以执行代码,即同一时刻只有一个线程使用CPU。执行单线程程序的开发人员看不到GIL的影响,但它可能是CPU绑定多线程代码中的性能瓶颈。

由于即使在具有多个CPU核心的多线程体系结构中,GIL一次只允许一个线程执行,因此GIL已经称为Python的“臭

名昭著”的特性。

GIL为Python解决了什么问题?

Python使用引用计数来进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量用于跟踪指向

该对象的引用数。当此技术达到0的时候,释放对象占用的内存

示例


import sys

a = []

b = a

sys.getrefcount(a) # 查看空列表的引用次数。

在上面的示例中,空列表对象的引用计数为3,a和b参数引用各一次,在调用sys.getrefcount()的时候也引用一次。

回到GIL:

问题是这个引用计数变量需要防止竞争条件,如果其中两个线程同时增加或减少其值。发生这种情况,它可能导致从未释放的内存泄漏,或者更糟糕的是,在对该对象的引用仍然存在时错误地释放内存。这可能会导致Python程序中出现崩溃或其他“怪异”错误。

通过向跨线程共享的所有数据结构添加,可以保持此引用计数变量的安全性,从而保证不会对它们进行不一致的修改。

但是为每个对象或对象组添加一个锁意味着将存在多个锁,这可能导致另一个问题 - 死锁(死锁只有在有多个锁时才会发生)。另一个问题是由于重复获取和释放锁而导致的性能下降。

GIL是解释器本身的一个锁,它增加了一条规则,即执行任何Python字节码都需要获取解释器锁。这可以防止死锁(因为只有一个锁)并且不会引入太多的性能开销。但它也让任何受CPU限制的Python程序都是单线程的。

GIL虽然被解释器用于其他语言(如Ruby),但并不是解决此问题的唯一方法。有些语言通过使用除引用计数之外的方法(例如垃圾收集)来避免需要GIL对线程安全内存管理。

另一方面,这意味着这些语言通常需要通过添加其他性能提升特性(如 JIT编译器 )来弥补GIL单线程性能优势的损失。

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

那么为什么要在Python中使用GIL呢?

自从操作系统没有线程概念以来,Python就已存在。Python的设计易于使用,以便更快地开发,越来越多的开发人员开始使用它。

有许多扩展正在为那些Python中需要其特性的C语言库而编写、服务。为了防止不一致的更改,这些C扩展需要GIL提供的线程安全内存管理。

GIL易于实现,很容易添加到Python中。它为单线程程序提供了性能提升,因为只需要管理一个锁。

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

正如您所看到的,GIL是一个实用的解决方案,可以解决CPython开发人员在Python生命中早期面临的一个难题。

对多线程的Python程序影响

当你在查看一个典型的Python程序——或任何计算机程序时,它们在性能上受CPU限制与受I / O限制是存在区别的。(这里的意思是说,不同的程序,限制它们的性能的原因是不一定相同的,有可能受到CPU限制,有可能受到I/O限制)

CPU绑定程序是那些将CPU推向极限的程序。这包括进行数学计算的程序,如矩阵乘法,搜索,图像处理等。

I / O绑定程序是花费时间等待输入/输出的程序,它可以来自用户,文件,数据库,网络等.I / O绑定程序有时需要等待很长时间才能从源头获取他们需要的东西,因为源可能需要在输入/输出准备好之前进行自己的处理,例如,用户考虑输入什么,或者在运行的数据库查询的过程。

让我们来看一个执行倒计时的简单CPU绑定程序:


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)

运行这段代码输出:


Time taken in seconds - ``3.368496894836426

现在使用两个并行线程将代码修改为相同的倒计时:

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)

再次运行:

Time taken in seconds - ``3.0929627418518066

可以看出,两个版本的完成时间几乎相同。在多线程版本中,GIL防止了CPU所绑定的线程并行执行GIL对I / O绑定的多线程程序的性能影响不大,因为线程在等待I / O时是共享锁的。

但是线程完全受CPU约束的程序,例如,使用线程处理部分图像的程序,不仅会因锁而成为单线程,而且与编写为完全单线程的场景相比,还会导致执行时间的增加,如上例所示。

这种增加是锁的获取和释放开销的结果。

为什么还没有删除GIL?

Python有许多的由C语言编写的扩展库,这些扩展库在很大程度上依赖于由GIL提供的解决方案,如果删除GIL,会导致现有的C扩展被破坏,许多地方需要重写甚至完全重写。

当然,还有其他解决方案可以解决GIL解决的问题,但有些解决方案是会降低单线程和多线程或者I/O绑定的程序的性能的。

Python3中对GIL的改进

Python 3为现有的GIL带来了重大改进 -

GIL会对 “仅CPU限制” 和 “仅I / O绑定” 的多线程程序造成影响,但是如果一个多线程程序,其中有些线程受I / O约束和有些线程受CPU约束会怎么样呢?

在这样的程序中,已知Python的GIL会使I / O绑定的线程匮乏,因为它们没有机会从CPU绑定的线程中获取GIL。

这是因为Python内置了一种机制,强制线程在连续使用固定间隔后释放GIL 如果没有其他人获得GIL,则相同的线程可以继续使用它。

>>>``import sys

>>>sys.getchectinterval() # 返回检查间隔值

100

|

这种机制的问题在于,大多数情况下,CPU绑定的线程会在其他线程获取GIL之前重新获取GIL。

在Python 3.2中修复了这个问题,并且添加了一种机制,可以查看被删除的其他线程的GIL获取请求数,并且在其他线程有机会运行之前不允许当前线程重新获取GIL。

如何处理Python的GIL

多进程与多线程:最流行的方法是使用多进程方法,使用多个进程而不是线程。每个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。Python有一个multiprocessing模块,可以轻松地创建进程:

|

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

|

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)

|

运行结果:

|

1

|

Time taken in seconds - ``2.558570623397827

|

与多线程版本相比,性能有了不错的提升。

耗费时间没有下降到上面看到的一半,是因为流程管理有自己的开销。多进程要比多线程的开销更多,这也可能会成为一个扩展瓶颈。

替代Python解释器: Python有多个解释器实现。分别用C,Java,C#和Python编写的CPython,Jython,IronPython和PyPy是最受欢迎的。GIL仅存在于CPython的原始Python实现中。如果程序及其库可用于其他实现之一,那么也可以尝试其他的Python解释器。

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

推荐阅读更多精彩内容