找回密码
 会员注册
查看: 17|回复: 0

什么是Python全局锁(GIL),如何避开GIL限制?

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-10 18:21:49 | 显示全部楼层 |阅读模式
一、什么是Python全局锁1、什么是全局锁?简单来说,Python全局解释器锁(GlobalInterpreterLock,简称GIL)是一个互斥锁(或锁),只允许一个线程保持Python解释器的控制权。这意味着在任何时间点都只能有一个线程处于执行状态。执行单线程程序的开发人员看不到GIL的影响,但它可能是CPU密集型和多线程代码中的性能瓶颈。由于GIL一次只允许一个线程执行,即使在具有多个CPU内核的平台上也是如此,因此GIL获得了Python“臭名昭著”功能的声誉。在本文中,您将了解GIL如何影响Python程序的性能,以及如何减轻它可能对代码产生的影响。2、GIL为Python解决了什么问题?Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有一个引用计数变量,该变量跟踪指向该对象的引用数量。当此计数达到零时,将释放对象占用的内存。让我们看一个简短的代码示例,以演示引用计数的工作原理:>>>importsys>>>a=[]>>>b=a>>>sys.getrefcount(a)312345在上面的示例中,空列表对象[]的引用计数为3。列表对象由a,b引用,参数传递给sys.getrefcount()。回到GIL,问题在于:此引用计数变量需要保护,以免受两个线程同时增加或减少其值时的负面影响。如果发生这种情况,可能会导致内存变量无法释放,或者更糟糕的是,当内存对象的引用计数不为零时,错误地释放了该对象。这可能会导致Python程序中出现崩溃或其他“奇怪”的错误。可以通过向跨线程共享的所有数据变量添加锁来确保此引用计数变量的安全,以避免不一致地修改它们。但是,向每个对象或对象组添加一个锁意味着将存在多个锁,这很容易导致另一个问题-死锁。另一个副作用是,重复获取和释放锁会导致性能下降。GIL是解释器本身上的单个锁,它添加了一个规则,即执行1个Python文件必须要获取解释器锁。这可以防止死锁(因为只有一个锁),并且不会引入太多的性能开销。但它有效地使任何CPU绑定的Python程序成为单线程。GIL虽然被解释者用于其他语言(如Ruby),但并不是这个问题的唯一解决方案。某些语言通过使用引用计数以外的方法(如垃圾回收)来避免线程安全内存管理的GIL要求,但这意味,这些语言通常必须通过添加其他性能提升功能(如JIT编译器)来补偿GIL的单线程性能优势的损失。3、为什么选择GIL作为解决方案?那么,为什么在Python中使用了一种看似如此阻碍性能提升的方法呢?这是Python开发人员的错误决定吗?用LarryHastings的话来说,GIL的设计决策是使Python像今天一样流行的因素之一。Python于1991年诞生,从操作系统没有线程概念的时代就已经存在了。Python被设计为易于使用,以使开发更快,越来越多的开发人员开始使用它。当时Python使用了许多C库,而且这些库的功能在Python中是必需的。为了防止不一致的更改,这些C扩展需要GIL提供的线程安全内存管理。GIL易于实现,并且很容易添加到Python中。它为单线程程序提供了性能提升,因为只需要管理一个锁。非线程安全的C库变得更容易集成。而这些C扩展成为Python容易被不同社区采用的原因之一。正如你所看到的,GIL是CPython开发人员在Python早期面临的一个难题的务实解决方案。4、对多线程Python程序的影响当您查看典型的Python程序或任何计算机程序时,性能受CPU限制的程序和受I/O限制的程序之间存在差异。CPU密集型程序是那些将CPU推向极限的程序。这包括进行数学计算的程序,如矩阵乘法、搜索、图像处理等。I/O绑定程序是花时间等待输入/输出的程序,这些输入/输出可能来自用户、文件、数据库、网络等。I/O绑定程序有时必须等待很长时间,直到它们从源获得所需的内容,因为源可能需要在输入/输出准备就绪之前进行自己的处理,例如,用户考虑在输入提示或在其自己的进程中运行的数据库查询中输入什么。让我们看一下执行倒计时的简单CPU密集型程序:``py#single_threaded.pyimporttimefromthreadingimportThreadCOUNT=50000000defcountdown(n):whilen>0:n-=1start=time.time()countdown(COUNT)end=time.time()print('Timetakeninseconds-',end-start)1234567891011121314154核CPU的测试机上运行结果如下$pythonsingle_threaded.pyTimetakeninseconds-6.2002403736114512现在修改了代码,以使用两个线程并行执行相同的倒计时代码:#multi_threaded.pyimporttimefromthreadingimportThreadCOUNT=50000000defcountdown(n):whilen>0:n-=1t1=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('Timetakeninseconds-',end-start)123456789101112131415161718192021再次运行,结果如下:$pythonmulti_threaded.pyTimetakeninseconds-6.92434263229370112如您所见,两个版本完成所需的时间几乎相同。在多线程版本中,GIL阻止CPU绑定线程并行执行。GIL对I/O绑定的多线程程序的性能没有太大影响,因为在线程等待I/O时,锁在线程之间共享。但是,线程完全受CPU限制的程序,例如,使用线程在部分中处理图像的程序,不仅会因为锁而变成单线程,而且执行时间也会增加,如上例所示,与编写为完全单线程的情况相比,多线程增加的时间是由获取锁和释放锁带来的开销。为什么不放弃GIL?Python的开发人员收到了很多关于此的投诉,但像Python这样流行的语言无法带来与删除GIL一样重要的变化,而不会导致向后不兼容问题。显然可以删除GIL,开发人员和研究人员过去已经多次这样做,但所有这些尝试都破坏了现有的C扩展,这些扩展在很大程度上依赖于GIL提供的解决方案。当然,对于GIL解决的问题还有其他解决方案,但其中一些会降低单线程和多线程I/O绑定程序的性能,其中一些太难了。毕竟,你不会希望你现有的Python程序在新版本发布后运行得更慢,对吧?Python的创建者和BDFLGuidovanRossum在2007年9月的文章“Itisn’tEasytoremovetheGIL”中向社区给出了答案:“只有当单线程程序(以及多线程但I/O绑定的程序)的性能不降低时,我才会欢迎一组补丁进入Py3k”从那以后的任何尝试都没有满足这个条件。为什么在Python3中没有删除它?Python3确实有机会从头开始启动许多功能,并在此过程中破坏了一些现有的C扩展,然后需要更新和移植更改以与Python3一起使用。这就是为什么早期版本的Python3被社区采用得较慢的原因。与Python2相比,删除GIL会使Python3在单线程性能方面变慢,您可以想象这会导致什么。您无法与GIL的单线程性能优势争论。所以结果是Python3仍然有GIL。但是Python3确实给现有的GIL带来了重大改进——我们讨论了GIL对“仅CPU绑定”和“仅I/O绑定”多线程程序的影响,但是某些线程受I/O约束而某些线程受CPU约束的程序呢?在此类程序中,众所周知,Python的GIL不会让I/O绑定线程缺乏,因为它们没有机会从CPU绑定线程获取GIL。这是因为Python中内置了一种机制,该机制强制线程在固定的连续使用间隔后释放GIL,如果没有其他人获得GIL,则同一线程可以继续使用。>>>importsys>>>#Theintervalissetto100instructions:>>>sys.getcheckinterval()1001234此机制中的问题是,大多数情况下,CPU绑定线程会在其他线程获取GIL之前重新获取GIL本身。这是大卫·比兹利(DavidBeazley)研究的,可视化可以在这里找到。这个问题在2009年的Python3.2中由AntoinePitrou修复,他添加了一种机制,可以查看其他线程丢弃的GIL获取请求的数量,并且不允许当前线程在其他线程有机会运行之前重新获取GIL。二、如何摆脱Python的全局锁的限制如果GIL给您带来了问题,您可以尝试以下几种方法来避开全局锁的限制1、使用多进程编程最流行的方法是使用多进程方法,其中使用多个进程而不是线程。每个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。Python有一个多进程multiprocessing模块,它让我们可以轻松地创建这样的进程:frommultiprocessingimportPoolimporttimeCOUNT=50000000defcountdown(n):whilen>0:n-=1if__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('Timetakeninseconds-',end-start)1234567891011121314151617'运行运行outputpythonmultiprocess.pyTimetakeninseconds-4.06024241447448712与多线程版本相比,性能有所提高,对吧?时间没有下降到我们上面看到的一半,因为流程管理有自己的开销。多个进程比多个线程重,因此请记住,这可能会成为扩展瓶颈。2、使用cython来避开全局锁cython通常用于处理计算密集型的任务,以加快python程序总体运行速度。如果你希望C风格的cython函数避开GIL限制,只需简单地使用withnogil参数cdefvoidsome_func()noexceptnogil:#函数功能代码....123cython的纯Python实现方式工,用装饰器来避开GIL@cython.nogil@cython.cfunc@cython.noexceptdefsome_func()->None:...12345在cython函数内,如果希望部分代码块不使用GIL,使用withnogil:语句defuse_add(n):result=1withnogil:foriinrange(n):result=add(result,result)returnresult1234567在cython函数中,可以指定一部分代码避免GIL,另一部分使用GILwithnogil:...#somecodethatrunswithouttheGILwithgil:...#somecodethatrunswiththeGIL...#somemorecodewithouttheGIL12345673、使用替代Python解释器Python有多个解释器实现。CPython,Jython,IronPython和PyPy,分别用C,Java,C#和Python编写,是最受欢迎的。GIL只存在于CPython的原始Python实现中。如果您的程序及其库可用于其他实现之一,那么您也可以尝试它们。当然使用非官方解释器的代价是:无法使用Cython.总结PythonGIL通常被认为是一个困难的话题。但python程序员通常只有在编写CPU密集型多线程代码时,才会受到它的影响。可以使用3种方法避免全局锁的限制:多进程,cython,使用非CPython解释器。Python未来版本传来好消息,据Python开发者透露:3.12版本,PEP703中新增了功能,Cython会将GIL做为可选项,即可以在cython中关闭GIL,而不是只在部分代码中通过withnogil来临时关闭.3.13中正在讨论通过在子线程增加subinterpreter方式,允许子线程运行在多个CPU内核上,听上去这个方式是可行的,虽然增加了一些开销,但对于多线程的性能提升还是明显的。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-7 06:55 , Processed in 2.720305 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表