async python两个_什么是Python全局解释器锁GIL(Global Interpreter Lock)?

Python全局解释器锁(GIL)是一个互斥锁,确保同一时间只有一个线程执行,避免内存管理问题。GIL对CPU受限的多线程程序造成性能瓶颈,但在I/O绑定程序中影响较小。尽管GIL限制了多核CPU的并行计算能力,但它确保了线程安全,使得C扩展易于集成。要应对GIL的影响,开发者可以使用多进程、替代Python解释器如Jython或PyPy,或者等待GIL的潜在解决方案如Gilectomy。
摘要由CSDN通过智能技术生成

10fa6ae83fc912b5abaf33332670e60f.png

本文翻译自:https://realpython.com/python-gil/

作者:Abhinav Ajitsaria 译者:@__bot__

简单来说,Python全局解释器锁(Global Interpreter Lock)或GIL是一个互斥锁,它只允许一个线程来控制Python解释器。

这意味着在任何时间点只有一个线程可以处于执行状态。执行单线程程序的开发人员感受不到GIL的影响,但它可能是CPU限制型和多线程代码中的性能瓶颈。

由于即使在具有多个CPU核心的多线程架构中,GIL一次只允许一个线程执行,因此GIL已经成为Python“臭名昭着”的特性。

在本文中,您将了解GIL如何影响Python程序的性能,以及如何减轻它可能对代码产生的影响。

GIL为Python解决了什么问题?

Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量用于跟踪指向该对象的引用数。当此计数达到零时,释放对象占用的内存。

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

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

在上面的示例中,空列表对象的引用计数为3。列表对象由a,b引用并且参数传递给sys.getrefcount()。

回到GIL:

问题是这个引用计数变量需要保护竞争条件。如果其中两个线程同时增加或减少其值,如果发生这种情况,它可能导致从未释放的内存泄漏,或者更糟糕的是,在对该对象的引用仍然存在时错误地释放内存。这可能会导致Python程序中出现崩溃或其他“怪异”错误。通过向跨线程共享的所有数据结构添加锁,可以保持此引用计数变量的安全性,从而不会对它们进行不一致的修改。

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

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

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

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

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

那么,为什么在Python中使用的方法看似如此阻碍呢?这是Python开发人员的糟糕决定吗?

好吧,用Larry Hastings的话来说, GIL的设计决定是让Python像今天一样受欢迎的原因之一。

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

开发人员正在为Python需要的功能编写许多C库扩展。为了防止不一致的更改,这些C扩展需要GIL提供的线程安全内存管理。

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

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

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

对多线程Python程序的影响

当您查看典型的Python程序或任何计算机程序时,那些在性能上受CPU限制的程序与受I / O限制的程序之间存在差异。

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

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)

在具有4个内核的系统上运行此代码,得到以下输出:

$ 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的开发人员对此有很多抱怨,但是像Python这样流行的语言不会带来像删除GIL那样重要的变化而不会导致向后不兼容问题。

显然可以删除GIL,过去开发人员和研究人员已多次执行此操作,但所有这些尝试都破坏了现有的C扩展,这些扩展在很大程度上依赖于GIL提供的解决方案。

当然,还有其他解决方案可以解决GIL解决的问题,但有些解决方案会降低单线程和多线程I / O绑定程序的性能,其中一些程序太难了。毕竟,你不希望现有的Python程序在新版本发布后运行得更慢,对吧?

Python的创建者和BDFL,Guido van Rossum,在2007年9月的文章“删除GIL并不容易”中给出了社区的答案:

只有当单线程程序(以及多线程但I / O绑定程序)的性能不降低时,我才欢迎使用Py3k中的一组补丁

此后的任何尝试都没有实现这一条件。

为什么不在Python 3中删除它?

Python 3确实有机会从头开始并在此过程中启动了许多功能,打破了一些现有的C扩展,然后需要更新并移植以使用Python 3.这就是为什么早期版本的Python 3看到社区采用较慢。

但为什么GIL并没有被删除?

删除GIL会使Python 3在单线程性能方面比Python 2慢,你可以想象会产生什么结果。你不能争论GIL的单线程性能优势。因此结果是Python 3仍然具有GIL。

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

我们讨论了GIL对“仅CPU限制”和“仅I / O绑定”多线程程序的影响,但是有些线程受I / O约束并且有些线程受CPU约束的程序怎么样呢?

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

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

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

这种机制的问题在于,大多数情况下,CPU密集型线程会在其他线程获取GIL之前重新获取GIL。这是由David Beazley研究的,可以在这里找到可视化。

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

如何处理Python的GIL

如果GIL导致您出现问题,可以尝试以下几种方法:

多进程与多线程:最流行的方法是使用多方法,使用多个进程而不是线程。每个Python进程都有自己的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有多个解释器实现。分别用C,Java,C#和Python编写的CPython,Jython,IronPython和PyPy是最受欢迎的。GIL仅存在于CPython的原始Python实现中。如果您的程序及其库可用于其他实现之一,那么您也可以尝试它们。

只需等待:虽然许多Python用户利用了GIL的单线程性能优势。多线程程序员不必烦恼,因为Python社区中一些最聪明的人正在努力从CPython中删除GIL。一种这样的尝试被称为Gilectomy。

Python GIL通常被认为是一个神秘而困难的话题。但请记住,作为Pythonista,如果您正在编写C扩展或者在程序中使用CPU绑定的多线程,则通常会受到它的影响。

在这种情况下,本文应该为您提供了GIL是什么以及如何在您自己的项目中处理它所需的一切。如果你想了解GIL的低级内部工作原理,我建议你观看由David Beazley 理解Python GIL的演讲。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值