文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2023-09-01 08:42

关注

一、什么是Python 全局锁

1、什么是全局锁?

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

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

由于 GIL 一次只允许一个线程执行,即使在具有多个 CPU 内核的平台上也是如此,因此 GIL 获得了 Python “臭名昭著”功能的声誉。

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

2、GIL为Python解决了什么问题?

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

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

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

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

回到GIL, 问题在于:此引用计数变量需要保护,以免受两个线程同时增加或减少其值时的负面影响。如果发生这种情况,可能会导致内存变量无法释放,或者更糟糕的是,当内存对象的引用计数不为零时,错误地释放了该对象。这可能会导致 Python 程序中出现崩溃或其他“奇怪”的错误。

可以通过向跨线程共享的所有数据变量添加来确保此引用计数变量的安全,以避免不一致地修改它们。

但是,向每个对象或对象组添加一个锁意味着将存在多个锁,这很容易导致另一个问题 - 死锁。另一个副作用是,重复获取和释放锁会导致性能下降。

GIL 是解释器本身上的单个锁,它添加了一个规则,即执行1个Python 文件必须要获取解释器锁。这可以防止死锁(因为只有一个锁),并且不会引入太多的性能开销。但它有效地使任何CPU绑定的Python程序成为单线程。

GIL 虽然被解释者用于其他语言(如 Ruby),但并不是这个问题的唯一解决方案。某些语言通过使用引用计数以外的方法(如垃圾回收)来避免线程安全内存管理的 GIL 要求,但这意味,这些语言通常必须通过添加其他性能提升功能(如 JIT 编译器)来补偿 GIL 的单线程性能优势的损失。

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

那么,为什么在Python中使用了一种看似如此阻碍性能提升的方法呢?这是Python开发人员的错误决定吗?

用Larry Hastings的话来说,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.pyimport timefrom threading import ThreadCOUNT = 50000000def countdown(n):    while n>0:        n -= 1start = time.time()countdown(COUNT)end = time.time()print('Time taken in seconds -', end - start)

4核CPU的测试机上运行结果如下

$ python single_threaded.pyTime taken in seconds - 6.20024037361145

现在修改了代码,以使用两个线程并行执行相同的倒计时代码:

# multi_threaded.pyimport timefrom threading import ThreadCOUNT = 50000000def countdown(n):    while n>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('Time taken in seconds -', end - start)

再次运行,结果如下:

$ python multi_threaded.pyTime 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月的文章“It isn’t Easy to remove the GIL” 中向社区给出了答案:

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

从那以后的任何尝试都没有满足这个条件。

为什么在Python 3中没有删除它?

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

与 Python 2 相比,删除 GIL 会使 Python 3 在单线程性能方面变慢,您可以想象这会导致什么。您无法与 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)研究的,可视化可以在这里找到。

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

二、如何摆脱 Python 的全局锁的限制

如果 GIL 给您带来了问题,您可以尝试以下几种方法来避开全局锁的限制

1、使用多进程编程

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

from multiprocessing import Poolimport timeCOUNT = 50000000def countdown(n):    while n>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('Time taken in seconds -', end - start)

output:

$ python multiprocess.pyTime taken in seconds - 4.060242414474487

与多线程版本相比,性能有所提高,对吧?

时间没有下降到我们上面看到的一半,因为流程管理有自己的开销。多个进程比多个线程重,因此请记住,这可能会成为扩展瓶颈。

2、使用 cython 来避开全局锁

cython通常用于处理计算密集型的任务,以加快python程序总体运行速度。

如果你希望C风格的cython函数避开GIL限制,只需简单地使用with nogil 参数

cdef void some_func() noexcept nogil:    # 函数功能代码    ....

cython的纯 Python实现方式工,用装饰器来避开GIL

@cython.nogil@cython.cfunc@cython.noexceptdef some_func() -> None:    ...

在cython函数内,如果希望部分代码块不使用GIL,使用with nogil: 语句

def use_add(n):    result = 1    with nogil:        for i in range(n):            result = add(result, result)    return result

在cython函数中,可以指定一部分代码避免GIL, 另一部分使用GIL

with nogil:    ...  # some code that runs without the GIL    with gil:        ...  # some code that runs with the GIL    ...  # some more code without the GIL

3、使用替代Python解释器

Python有多个解释器实现。CPython,Jython,IronPython和PyPy,分别用C,Java,C#和Python编写,是最受欢迎的。GIL 只存在于 CPython 的原始 Python 实现中。如果您的程序及其库可用于其他实现之一,那么您也可以尝试它们。
当然使用非官方解释器的代价是:无法使用Cython.

总结

Python GIL通常被认为是一个困难的话题。但python程序员通常只有在编写CPU密集型多线程代码时,才会受到它的影响。

可以使用3种方法避免全局锁的限制: 多进程,cython,使用非CPython解释器。

Python未来版本传来好消息 ,据 Python开发者透露:

来源地址:https://blog.csdn.net/captain5339/article/details/131375952

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯