python学习_Python中的GIL是什么


楔子

image-20220519145508151

一直听说GIL是python的弱点,工作过程中也只了解个皮毛,偶然看到一篇外文讲的很好,因此翻译搬运到这里,如有不足,欢迎提出。

什么是Python的全局解释锁(Global Interpreter Lock (GIL))

Python全局解释锁,或者叫 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需要解决的问题是:在两个线程同时想要改变某对象的引用计数值的情况下,如何正确的进行处理以保证程序运行正常。假设没有正确的处理这种情况,它可能会导致内存泄漏问题(对象占用的内存永远不会释放),或者更糟糕的是,在对该对象的引用仍然存在时错误地释放内存。这可能会导致 Python 程序崩溃或其他莫名其妙的错误。

这个引用计数变量可以通过向所有线程共享的数据结构(对象)添加来保持安全,这样它们就不会被不一致地修改。

但是为每个对象或对象组添加一个锁意味着将存在多个锁,这可能会导致另一个问题——死锁(死锁只有在存在多个锁时才会发生)。另一个副作用是由于不同线程重复获取和释放锁会导致性能下降。(解决锁竞争需要通过机制协调,这个过程会增加性能开销)

GIL 是Python解释器本身自带的锁机制,它添加了一条规则,即对任何 Python 字节码的执行都需要获取全局解释器锁(GIL)。好处是这可以防止死锁(因为只有一个锁)并且不会引入太多的性能开销。坏处是它有效地使任何受 CPU 限制的 Python 程序成为单线程的(多线程无法利用多核CPU并行计算的优势,仍只能每个时间点只利用一个CPU)。

虽然 Ruby 等其他语言的解释器也使用了GIL,但并不是说线程安全的问题只能用GIL解决。某些语言通过使用引用计数以外的方法(例如垃圾收集)来避免 GIL 对线程内存安全管理的要求。

换句话说,这通常意味着不使用GIL的语言必须通过添加其他性能提升功能(如 JIT 编译器)来弥补 GIL 单线程性能优势的损失。

(译者:也就是说GIL是把双刃剑:对于单线程程序会很快,但是损失了多线程并行计算的能力;不使用GIL虽然解决了多线程并行计算的问题,但是会增加单线程运行时的性能开销。)

image-20220519145105949

为什么Python仍然选择 GIL 作为解决方案呢?

那么,为什么 Python 选择了一种看似如此不便利的方法呢?这对开发 Python 语言的人员来说算是做出了一个错误的决定吗?

嗯,用Larry Hastings 的话来说, 设计使用 GIL 正是是让 Python 像今天这样流行的原因之一。

Python 在操作系统还没有线程这个概念的时候就存在了。 语言被发明的初衷就是要易于使用,以便可以更快的进行开发工作,这一点也让越来越多的开发人员开始使用它。

在用C语言开发Python的过程中,开发人员为现有的 C 语言库编写了许多扩展,因为这些扩展库的特性在 Python 中是必需的。为了防止不一致的更改,这些 C 扩展需要使用 GIL 提供的线程安全的内存管理机制来保证。

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

GIL使得非线程安全的 C 库变得更容易集成。而正是这些对 C 库的扩展(使用了GIL)成为 Python 被不同社区欣然采用的原因之一。

因此可以看出,GIL 是CPython开发人员针对在 Python 早期面临的难题的实用解决方案。

对 Python 多线程程序的影响

在你查看一个典型的 Python 程序或任何计算机程序时,一般情况下,性能瓶颈主要体现在两个方面:

  • CPU密集型程序——受 CPU 能力限制
  • IO密集型程序——受 I/O 能力限制

CPU 密集型程序是那些将 CPU 推到使用极限的程序(即频繁使用CPU的程序)。包括进行数学计算的程序,如矩阵乘法、搜索、图像处理等。

I/O 密集型程序是那些需要花费时间来等待用户、文件、数据库、网络等输入输出(Input/Output)的程序。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 核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

image-20220519145410239

如您所见,两个版本的完成时间几乎相同。在多线程版本中,GIL 阻止了CPU密集型程序的线程并行执行。

GIL 对 I/O 密集型的多线程程序性能没有太大影响,因为在线程等待 I/O 时,锁是在线程之间共享的。

当程序是完全的 CPU 密集型程序时,比如处理图像的程序,假如使用Python的多线程去做,不仅程序会因为GIL而变成单线程,而且还会看到执行时间增加了,如上面的示例所示。

这是因为与完全单线程的情况相比,多线程增加了锁的获取和释放的步骤,造成了更多的性能开销的结果。

为什么 GIL 还没有被移除?

Python 的开发人员收到了很多关于GIL的抱怨,但是像 Python 这样流行的语言如果删除了 GIL机制 ,这么大的变更如果向后兼容出现了问题,这是不能接受的。

GIL 当然是可以被删除的,过去开发人员和研究人员已经多次这样尝试,但所有这些尝试都破坏了现有的 C 扩展,因为这些扩展严重依赖于 GIL 提供的解决方案。

当然,还有其他解决方案可以替代GIL解决C扩展面临的问题,但其中一些会降低单线程和多线程 I/O 密集型程序的性能,另外一些则太难了。毕竟,您不希望现有的 Python 程序更新新版本python后运行速度变慢,对吧?

Python 的创始人和 BDFL(Benevolent Dictator For Life,终身仁慈独裁者),Guido van Rossum,于 2007 年 9 月在他的文章“移除 GIL 并不容易”中对社区做出了回答:

“只有在单线程程序(以及多线程 I/O 密集型的程序)的性能不降低的情况下,我才会同意在 Python3 中打补丁(解决GIL造成的问题)”

从那以后所做的任何试图解决GIL的尝试都没有满足这一条件。

为什么GIL没有在 Python 3 中被移除?

Python 3 (相较Python 2)确实从头开始增加了很多特性,并且在这个过程中,也确实破坏了一些现有的 C 扩展——这些扩展需要更新和移植才能与 Python 3 一起使用。这就是为什么Python 3 的早期版本被社区采用速度较慢的原因。

但是为什么 GIL 没有在这个过程中被移除呢?

与 Python 2 相比,删除 GIL 后在单线程性能方面会使 Python 3 变慢,您可以想象这会导致什么结果。目前还没有解决方案能替代 GIL 在单线程性能上的优势。所以结果 Python 3 仍然保有 GIL。

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

我们上面讨论的 GIL 对多线程的影响,仅仅是对“纯粹的CPU密集型”和“纯粹的I/O 密集型”的多线程程序的影响,但是实际的程序往往没那么“纯粹”,那么GIL 对于某些线程是CPU密集而某些线程是IO密集的多线程程序的影响又是怎么样的呢?

在这样的程序中,可以想象,Python 的 GIL 会饿死 I/O 密集型线程,因为它们得不到从 CPU 密集型线程获取 GIL 的机会。

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

>>> import sys
>>> # The interval is set to 100 instructions:(这个间隔被设置为100个指令执行时间)
>>> sys.getcheckinterval()
100

这种机制的问题在于,大多数情况下, CPU 密集型的线程会在其他线程获取 GIL 之前重新获取 GIL。这种机制 David Beazley 做了相关研究,可以在此处找到可视化的内容。

这个问题在 2009 年的 Python 3.2 中由 Antoine Pitrou 修复,他添加了一种机制,可以查看其他线程被丢弃的 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 有多个解释器实现。分别用CJava 、C# 和 Python编写的CPython、Jython、IronPython 和PyPy是最受欢迎的。GIL 仅存在于 CPython 的原始 Python 实现中。如果您的程序及其依赖库可用其他解释器实现,那么您也可以尝试它们。

做等等党:虽然许多 Python 用户享受了 GIL 带来的单线程性能优势。但是多线程程序员也不必担心,因为 Python 社区中有一些最聪明的人正在努力从 CPython 中删除 GIL。其中一个比较出名尝试叫Gilectomy

Python 的GIL 通常被认为是Python 的一个疑难杂症。但请记住,作为 Pythonista,您通常只有在编写 C 扩展或在程序中使用CPU密集型的多线程时才会受到它的影响。

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

总结

本文在2018年前就写出来了,阅读时应考虑到Python在这个时间下的飞速发展~

原文翻译自 https://realpython.com/python-gil/,目的是技术分享,如有侵权,请联系我删除。

希望大家能有所收获~

posted @ 2023-12-15 18:06  故君子慎为善  阅读(19)  评论(0)    收藏  举报