一文详解 Python GIL 设计

简介: Global Interpreter Lock (以下简称 GIL),简单来说,就是一把锁。这把锁在任意时刻只允许一个 Python 进程使用 Python 解释器。也就是任意时刻,Python 只有一个线程在运行。

v2-57476436a61e92deac65cdbe88507ae6_1440w.jpg


这篇文章详细的解释一下 Python 里面的全局解释器锁(Global Interpreter Lock,GIL)。


一、GIL 是什么?



Global Interpreter Lock (以下简称 GIL),简单来说,就是一把锁。这把锁在任意时刻只允许一个 Python 进程使用 Python 解释器。


也就是任意时刻,Python 只有一个线程在运行。


这种 GIL 锁的设计对于只写单线程运行的程序员来说其实没有什么影响(如果你还不知道什么是多线程,那你写的就是单线程)。但是对于计算密集型的程序(CPU-bound)和基于多线程的程序来说,Python 的 GIL 设计很有可能会造成性能瓶颈。


因为在任意时刻 GIL 都只允许一个线程运行,即使你的代码跑在多核 CPU 的电脑上也是如此。因此 GIL 在某些人眼里成为了 Python 里一个臭名昭著的特性。


这篇文章主要说一下 GIL 是如何影响我们写的代码的,以及我么可以怎么样尽量减少 GIL 给我们造成的影响。


二、为什么 Python 要引入 GIL?



凡事都是有原因的,如果 GIL 只有坏处没有好处,那么咱们的 Python 之父肯定不会闲的没事干引入一个没啥用的东西的。


GIL 解决了什么问题?在回答这个问题之前,我们先来聊聊 Python 里面的垃圾回收机制(引用计数)。


引用计数

Python 使用引用计数来进行内存管理(垃圾回收)。引用计数就是说 Python 里创建的所有对象,Python 都有一个变量(reference count)记录着当前有多少个引用指向了这个对象,当引用数变成 0 的时候,Python 就会回收这个对象所占用的内存。


来看一个简单的例子:


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


在上面这个简单的例子中,指向这个空 list 对象的引用计数是 3。a,b 和 传入sys.getrefcount 的参数(传参的时候会拷贝一次地址)这 3 个对象指向了空 list。

关于 Python 如何解决循环引用下的内存回收问题比较复杂,这里就不展开了。

如果你感兴趣的话也可以看一下 None 这个对象有多少个对象指向了它。


好,我们说回到 GIL。


Python 的引用计数需要避免资源竞争的问题,我们需要在有两个或多个线程同时增加或减少引用计数的情况下,依然保证引用计数的结果是正确的。


当有多个线程同时改一个对象的引用计数的时候,有可能导致内存泄漏(对象的引用计数永远没有归零的机会),还有可能导致对象提起释放,触发莫名奇妙的 bug,程序崩溃(一个对象存在引用的情况下引用计数变成了 0,导致此对象提前释放)。


通过对不同线程访问、修改引用计数增加锁,我们就可以保证引用计数总是被正确的修改(可以联想一下数据库的锁机制)。


但是,如果我们对每一个对象或者每一组对象都增加锁,这就意味在在你的 Python 程序中有很多个锁同时存在。多个锁同时存在会有其他的风险--死锁(死锁只会在有多个锁存在的情况下发生,参考数据的的死锁)。


除此之外,性能下降也是多个锁存在的一大弊端。因为申请锁和释放锁都是一笔不小的开销。


GIL 是一个锁,或者一把锁(这里强调单个),这把锁加载了 Python 的解释器上,它要求任何 Python 代码在执行的时候需要先申请这把锁,否则就别想执行。


只有一把锁,带来的好处就是 1、不会有死锁,2、对因为引入锁而导致的性能下降影响不大,然而坏处就是 GIL 这把锁让计算密集型的代码也只能使用单线程执行。


GIL 并不是解决资源竞争的唯一解法,一些其他的语言不使用引用计数,使用别的方法也能达到线程安全地内存管理。


当然,鱼和熊掌不可兼得,其他语言不使用 GIL,就享受不到使用 GIL 带来的单线程性能提升,就得像个其他办法提升单个线程的性能了,比如 JIT compilers。


三、Python 为什么选择了 GIL ?



那么,Python 引入 GIL 是不是一个聪明的决定?


根据 Python 大牛 Larry Hastings 的说法: Python 引入 GIL 是使其如此流行的主要原因之一。


在操作系统还没有线程(threads)这个概念的时候,Python 就已经有了。Python 设计的理念就是易于使用,方便程序员快速地构建自己的程序。


GIL 实现简单,引入 Python 也很简单。因为引入 GIL 只是引入了的一把而不是多把锁,所以 GIL 提供了单线程程序的高性能。


有了 GIL,即使不是线程安全的 C 语言库,也能很容易的引入到 Python 代码中。能够引入这些 C 语言库也是 Python 如今这样火的一个重要原因(C 语言能提供更高的性能)。


正如上面所提到的那样,GIL 通过提供一种程序上的方法解决了 CPython 开发者以前遇到的线程不安全的开发问题,这是引入 GIL 的一大优点。


四、GIL 对 Python 多线程开发者的影响



在提到开发性能瓶颈的时候,我们经常把对资源的限制分为两类,一类是计算密集型(CPU-bound),一类是 I/O 密集型(I/O-bound)。


计算密集型的程序是指的是把 CPU 资源耗尽的程序,也就是说想要提高性能速度,就需要提供更多更强的 CPU,比如矩阵运算,图片处理这类程序。


I/O 密集型的程序只的是那些花费大量时间在等待 I/O 运行结束的程序,比如从用户指定的文件中读取数据,从数据库或者从网络中读取数据,/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 - 3.4321422576904297


上面的示例代码是一个简单的单线程程序,下面稍加改造,把上面的代码改成使用两个线程的版本。


# 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 - 3.9487345218658447


可以看到,这两个版本的代码用到的时间基本上是一样的。因为在多线程的版本中,GIL 提供了线程锁,导致在任意时刻只有一个线程在运行。所以最后运行的时间差不多。


但是 GIL 对 I/O 密集型的程序没有什么性能影响,因为当一个线程等待 I/O 结束的时候,这个线程的 GIL 锁会释放,其他的线程可以运行。


但是对于一个完全是计算密集型的程序来说,GIL 锁就会严重的影响性能了。


五、为什么 GIL 锁至今都没有被移除?



就 GIL 这一特性而言,Python 的开发者已经收了无数的抱怨。但是作为一门如此广泛使用的语言,做出变更一定是慎重考虑的,而且对于向后不兼容变更,一般都会避免。

前面说到,GIL 实现简单,添加简单。那么其实对应的,如果只是单纯的移除 GIL,也很容易。以前也有很多开发者尝试把 GIL 从 Python 里面移除,然而一旦移除之后很多 Python 依赖的 C 语言库就没法用了(因为这些 C 语言库严重依赖 GIL 特性)。


当然,想要解决 GIL 所解决的问题,不止有 GIL 一种办法。但是其他的方法要么实现起来过于复杂,要么严重降低单线程代码的性能。毕竟,你肯定不希望在升级了 Python 版本之后,你原来的代码运行效率降低吧!


Python 的创造者在它的一篇文章(It isn’t Easy to remove the GIL)中回复了有关移除 GIL 的问题,原话是:


I’d welcome a set of patches into Py3k only ifthe performance for a single-threaded program (and for a multi-threaded but I/O-bound program) does not decrease.


然而不降低性能的移除 GIL ,通过提供其他途径完成 GIL 所完成的事,至今都还有完全实现。


六、为什么不利用发布 Python3 这个版本的机会移除 GIL?



对于 Python 这门语言而言,Python3 称得上是 Python 的一次从头开始。因此 Python3 确实有机会除掉 GIL,移除掉那些对 GIL 有依赖的 C 语言库,或者重写这些 C 语言库。这也是为什么 Python3 的早期版本被 Python 社区采用的速度较慢的原因。

那 Python3 为啥还是把 GIL 给留下来了呢?


因为移除 GIL 会导致单线程性能 Python3 比不过 Python2(你可以想像一下这对推广 Python3 有什么影响)。GIL 对单线程性能的好处这是谁都无法否认的,因此 Python3 保留了 GIL 的设计。


但是 Python3 还是针对 GIL 做了很多优化。


上面我们说性能瓶颈的时候,谈到了代码有计算密集型和 I/O 密集型,那么如果一个程序既是计算密集型又是 I/O 密集型呢?


在上面这种情况下,Python3 的 GIL 知道他更应该把 GIL 分给计算密集型的线程,而不是所有线程一视同仁的对待(也就是饿死 I/O 密集线程)。


这种机制的实现原理是 Python 有一种机制强制获取到 GIL 锁的线程交在规定的时间段长之后交出 GIL 锁,如果此时没有其他线程申请锁,那么远来的线程可以继续持有锁,继续运行。


import sys
sys.getswitchinterval()
0.005 # 线程可以持有锁的时间,单位秒
# python3.8 
# python3.7 使用sys.getcheckinterval()


然而这种机制带来新的问题是大部分的 CPU-bound 线程都会在其他线程申请 GIL 锁之前再次申请 GIL 锁,这会导致有些线程永远拿不到 GIL 锁(我们虽然希望 CPU 密集型的线程能相比 I/O 密集型的线程能更长时间的获得锁,但是并不希望 CPU 密集型拿着锁永远不释放)。


这个问题在 Python3.2 版本通过引入记录没有获取到 GIL 锁线程线程请求 GIL 锁的次数的机制解决了,详情可以参考这篇文章


七、Python 开发者如何与 GIL 和谐的共存?



如果 GIL 真的对你的代码性能有很大的影响,那么可以尝试下面这些方法。


1、多进程


解决 GIL 锁最通用的方法是使用多进程。因为每个 Python 进程都有自己的 Python 解释器,有自己的内存空间,有自己的 GIL 锁,相互之间没有影响。自然也就没有问题了。


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 - 1.9049170017242432


可以看到运行时间明显的下降了。


这里要说明一下的是时间并没有降到原来的一半,这是因为进程管理,切换也是要花费时间的。多进程程序相比单线程程序需要的资源开销更多,资源管理和回收的时间也需要的更多。


2、换 Python 解释器



Python 的解释器不止一个,而是有很多个。包括 CPython, Jython, IronPython and PyPy,分别是用 C,Java,C#,Python 实现的。GIL 锁只存在于 CPython 解释器中。如果你的代码和依赖库不依赖 CPython 的话,那么换其他的解释器也是可行的方案。

还有个称不上方法的方法,那就是等一等。因为 Python 社区有很多开发者正在致力于移除 CPython 中的 GIL 锁。


有任何问题欢迎评论留言,如果本文有用,也欢迎点个赞~~~

目录
相关文章
|
2月前
|
设计模式 开发者 Python
Python编程中的设计模式:工厂方法模式###
本文深入浅出地探讨了Python编程中的一种重要设计模式——工厂方法模式。通过具体案例和代码示例,我们将了解工厂方法模式的定义、应用场景、实现步骤以及其优势与潜在缺点。无论你是Python新手还是有经验的开发者,都能从本文中获得关于如何在实际项目中有效应用工厂方法模式的启发。 ###
|
2月前
|
设计模式 算法 搜索推荐
Python编程中的设计模式:优雅解决复杂问题的钥匙####
本文将探讨Python编程中几种核心设计模式的应用实例与优势,不涉及具体代码示例,而是聚焦于每种模式背后的设计理念、适用场景及其如何促进代码的可维护性和扩展性。通过理解这些设计模式,开发者可以更加高效地构建软件系统,实现代码复用,提升项目质量。 ####
|
2月前
|
设计模式 监控 算法
Python编程中的设计模式应用与实践感悟###
在Python这片广阔的编程疆域中,设计模式如同导航的灯塔,指引着开发者穿越复杂性的迷雾,构建出既高效又易于维护的代码结构。本文基于个人实践经验,深入探讨了几种核心设计模式在Python项目中的应用策略与实现细节,旨在为读者揭示这些模式背后的思想如何转化为提升软件质量的实际力量。通过具体案例分析,展现了设计模式在解决实际问题中的独特魅力,鼓励开发者在日常编码中积极采纳并灵活运用这些宝贵的经验总结。 ###
|
2月前
|
设计模式 监控 数据库连接
Python编程中的设计模式之美:提升代码质量与可维护性####
【10月更文挑战第21天】 一段简短而富有启发性的开头,引出文章的核心价值所在。 在编程的世界里,设计模式如同建筑师手中的蓝图,为软件的设计和实现提供了一套经过验证的解决方案。本文将深入浅出地探讨Python编程中几种常见的设计模式,通过实例展示它们如何帮助我们构建更加灵活、可扩展且易于维护的代码。 ####
|
2月前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践感悟####
本文作为一篇技术性文章,旨在深入探讨Python编程中设计模式的应用价值与实践心得。在快速迭代的软件开发领域,设计模式如同导航灯塔,指引开发者构建高效、可维护的软件架构。本文将通过具体案例,展现设计模式如何在实际项目中解决复杂问题,提升代码质量,并分享个人在实践过程中的体会与感悟。 ####
|
3月前
|
设计模式 开发者 Python
Python编程中的设计模式:从入门到精通####
【10月更文挑战第14天】 本文旨在为Python开发者提供一个关于设计模式的全面指南,通过深入浅出的方式解析常见的设计模式,帮助读者在实际项目中灵活运用这些模式以提升代码质量和可维护性。文章首先概述了设计模式的基本概念和重要性,接着逐一介绍了几种常用的设计模式,并通过具体的Python代码示例展示了它们的实际应用。无论您是Python初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和实用的技巧。 ####
|
7月前
|
分布式计算 并行计算 安全
在Python Web开发中,Python的全局解释器锁(Global Interpreter Lock,简称GIL)是一个核心概念,它直接影响了Python程序在多线程环境下的执行效率和性能表现
【6月更文挑战第30天】Python的GIL是CPython中的全局锁,限制了多线程并行执行,尤其是在多核CPU上。GIL确保同一时间仅有一个线程执行Python字节码,导致CPU密集型任务时多线程无法充分利用多核,反而可能因上下文切换降低性能。然而,I/O密集型任务仍能受益于线程交替执行。为利用多核,开发者常选择多进程、异步IO或使用不受GIL限制的Python实现。在Web开发中,理解GIL对于优化并发性能至关重要。
74 0
|
3月前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践###
【10月更文挑战第18天】 本文深入探讨了Python编程中设计模式的应用与实践,通过简洁明了的语言和生动的实例,揭示了设计模式在提升代码可维护性、可扩展性和重用性方面的关键作用。文章首先概述了设计模式的基本概念和重要性,随后详细解析了几种常用的设计模式,如单例模式、工厂模式、观察者模式等,在Python中的具体实现方式,并通过对比分析,展示了设计模式如何优化代码结构,增强系统的灵活性和健壮性。此外,文章还提供了实用的建议和最佳实践,帮助读者在实际项目中有效运用设计模式。 ###
30 0
|
3月前
|
设计模式 存储 数据库连接
Python编程中的设计模式之美:单例模式的妙用与实现###
本文将深入浅出地探讨Python编程中的一种重要设计模式——单例模式。通过生动的比喻、清晰的逻辑和实用的代码示例,让读者轻松理解单例模式的核心概念、应用场景及如何在Python中高效实现。无论是初学者还是有经验的开发者,都能从中获得启发,提升对设计模式的理解和应用能力。 ###
|
4月前
|
设计模式
python24种设计模式
python24种设计模式