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

这篇具有很好参考价值的文章主要介绍了什么是Python全局锁(GIL),如何避开GIL限制?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、什么是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.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核CPU的测试机上运行结果如下

$ 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月的文章“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 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)

output:

$ python multiprocess.py
Time 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.noexcept
def 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://www.toymoban.com/news/detail-806279.html

  • 3.12版本, PEP703中新增了功能,Cython 会将GIL做为可选项,即可以在cython中关闭 GIL,而不是只在部分代码中通过with nogil 来临时关闭.
  • 3.13 中正在讨论通过在子线程增加 subinterpreter 方式,允许子线程运行在多个CPU内核上,听上去这个方式是可行的,虽然增加了一些开销,但对于多线程的性能提升还是明显的。

到了这里,关于什么是Python全局锁(GIL),如何避开GIL限制?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Python黑魔法揭秘:装饰器、生成器、异步编程、GIL、描述符和元类

    Python中的某些特性被看作是“黑魔法”,原因在于它们的强大功能和复杂性。接下来,让我们深入探索这些特性。 装饰器是修改函数或类行为的强大工具,它提供了一种可读性强、代码重用的方式来增强或修改函数或类的行为。装饰器就像一个包裹原函数或类的外壳,能够在

    2024年02月14日
    浏览(46)
  • GIL 锁或将在 CPython 中成为可选项

    哈喽大家好,我是咸鱼 几天前有媒体报道称,经过多次辩论,Python 指导委员会打算批准通过 PEP 703 提案, 让 GIL(全局解释器)锁在 CPython 中成为一个可选项 PEP 703 提案主要目标是使 GIL 变成可选项,即允许 Python 解释器在特定情况下不使用GIL 这将允许 Python 在多核处理器上

    2024年02月13日
    浏览(28)
  • Python代码打包成EXE可执行文件(避开打包文件太大的坑)

    ​​​​​​​ 目录 一、博主的成长经历  二、虚拟环境下打包的好处  三、pyinstaller的基础用法 四、虚拟环境打包操作实例 五、成果展示 欢迎大家来观栏~  ——随乔木凉夏 博主最初使用pyinstaller打包py文件的时候,用的很是顺心,命令行复制粘贴,回车键一敲,不用多久

    2024年02月09日
    浏览(48)
  • Python中如何实现跨文件全局变量的访问?

    Python中如何实现跨文件全局变量的访问? 在Python开发中,我们常常需要在多个文件中访问同一个全局变量。但是由于Python的作用域规则,我们无法直接在其他文件中使用没有声明过的变量。那么如何才能实现在不同文件中共享同一个全局变量呢?本文将为你详细介绍Python中跨

    2024年02月10日
    浏览(39)
  • 如何在 Python 中执行 MySQL 结果限制和分页查询

    限制结果数量 示例 1: 获取您自己的 Python 服务器 选择 \\\"customers\\\" 表中的前 5 条记录: 如果您想返回从第三条记录开始的五条记录,可以使用 \\\"OFFSET\\\" : 示例 2: 从位置 3 开始,返回 5 条记录 示例 注意:您可以使用JOIN代替INNER JOIN,它们都会给您相同的结果。 在上面的示

    2024年02月05日
    浏览(51)
  • 代理ip全局代理是什么且如何设置

        在网络通信中,代理是一种常见的技术,它充当客户端与目标服务器之间的中间人,接收和转发请求。而 代理ip全局代理是一种特殊的代理设置,它可以将所有的网络请求通过代理服务器进行转发,而不仅仅是特定的应用程序或浏览器。       下面就让我们一起来了解一

    2024年02月11日
    浏览(52)
  • chatgpt赋能python:Python循环等待:什么是它?如何解决?

    在 Python 编程中,循环等待是一种常见的问题。它发生在代码一直等待某个操作的结果,而这个结果却永远不会到来。这种情况会导致程序停顿或挂起,从而影响整个应用程序。 循环等待通常指的是多个线程或进程之间的相互等待。当一个线程需要另一个线程的结果时,它会

    2024年02月06日
    浏览(59)
  • 【python技巧】什么是虚拟环境?以及如何配置虚拟环境

    一般情况一台机器上只能安装一个应用程序,但python可以安装很多遍,并可以安装在任意位置。在安装插件的时候,需要选择是给哪套程序安装插件。python使用这种方法不仅实现了第三方插件的相互隔离, 也实现了在同一台机器上配出多个各具特色的python环境。 我们安装p

    2023年04月13日
    浏览(55)
  • Vue中 全局限制输入特殊字符

    传送门:Vue实现自定义指令(directive)及应用场景 背景:开发中遇到的表单输入,常常会限制特殊字符的输入 以满足安全性测试的要求。 1. 单独处理每个文本框 这样每个输入框单独处理,工作量较大且不好维护,所以需要自定义一个指令来统一实现这一需求。 2. 自定义指

    2024年02月15日
    浏览(47)
  • 什么是Python爬虫分布式架构,可能遇到哪些问题,如何解决

    目录 什么是Python爬虫分布式架构 1. 调度中心(Scheduler): 2. 爬虫节点(Crawler Node): 3. 数据存储(Data Storage): 4. 反爬虫处理(Anti-Scraping): 5. 分布式通信和协调(Communication and Coordination): Python爬虫分布式架构代码示例 1. 调度中心(scheduler.py): 2. 爬虫节点(crawl

    2024年02月10日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包