Python 的 GIL 消灭了吗?

2024年 5月 24日 55.9k 0

导读:历史上的GIL特性是为帮助开发者少走弯路的,如今它却成了性能的瓶颈。在最新的Python中,有意想把它拿下,但是又谈何容易,包括遗留代码,与其它新特性的权衡。

我们回到2003的年初,中央处理器芯片厂商英特尔推出了新款Pentium 4“HT”处理器。该处理器的主频为 3 GHz,并具有当时先进的“超线程”技术。

在接下来的几年里,英特尔和 AMD 通过提高总线速度、二级缓存大小和减小芯片尺寸,以最大限度地减少延迟,来实现最佳台式计算机的性能。

3Ghz HT 于 2004 年被主频高达 4 GHz 的“Prescott”型号 580 取代。似乎获得更好性能的道路是更高的时钟速度,但 CPU 却受到高功耗与地球变暖的热量输出困扰。

现在你的台式机中还有 4Ghz CPU 吗?估计不太可能了,因为提高性能的方法是更高的总线速度和多核心。Intel Core 2 于 2006 年取代了 Pentium 4,但时钟速度要低得多。

除了消费级多核 CPU 的发布之外,2006 年还发生了一件事情:Python 2.5 发布!它捆绑了开发者熟悉并喜爱的 with 语句的测试版。

当使用 Intel 的 Core 2 或 AMD 的 Athlon X2 时,当时的Python 2.5 有一个主要限制,那就是GIL。

Python 的 GIL 消灭了吗?-1

什么是 GIL?

GIL(即全局解释器锁)是 Python 解释器中的布尔值,受互斥体保护。

CPython 解释器中的核心字节码评估循环使用锁,来设置当前正在执行语句的线程。

CPython 支持单个解释器内的多个线程,但线程必须请求访问 GIL 才能执行操作码(低级操作)。

反过来,这意味着 Python 开发人员可以使用异步代码、多线程代码,而不必担心获取任何变量的锁或进程因死锁而崩溃。

GIL 使 Python 中的多线程编程变得简单。GIL 还意味着虽然 CPython 可以是多线程的,但在任何给定时间只能执行 1 个线程。这意味着你的四核 CPU 正在执行此操作 —(希望能排除蓝屏)

GIL 的当前版本是在 2009 年编写,用于支持异步功能,即使在多次尝试删除它或减少对它的要求之后,它仍然相对没有受到影响。

任何删除 GIL 的提案的要求是它不应降低任何单线程代码的性能。在 2003 年启用过超线程的开发者都会明白为什么它很重要。

Python 的 GIL 消灭了吗?-2

如何避免 CPython 中的 GIL

如果你想在 CPython 中实现真正的并发代码,则必须使用多进程。

在 CPython 2.6 中,多处理模块被添加到标准库中。多重处理是 CPython 进程生成的包装器(每个进程都有自己的 GIL)。来看类似如下代码:

from multiprocessing import Processdef f(name):    print 'hello', nameif __name__ == '__main__':    p = Process(target=f, args=('bob',))    p.start()    p.join()

这样就可以生成新进程,通过编译的 Python 模块或函数发送命令,然后重新加入到主进程。

多进程处理还支持通过队列或管道共享变量。它还存在一个 Lock 对象,用于锁定主进程中的对象,以供其它进程的写入。

但是多处理进程还有一个很大缺陷:就是它在时间和内存使用方面都有很大的开销, CPython 启动时间大概是 100–200 毫秒。

因此,虽然你可以在 CPython 中使用并发代码,但必须仔细规划其应用程序,以适应长时间运行的进程,这些进程之间没有任何共享对象。

还有另一种选择是,像使用 Twisted 这样的第三方包。

PEP554 和 GIL 的死亡

不妨回顾一下,CPython 中实现多线程很容易,但它不是真正的并发,多处理是并发的,但是开销又很大。

如果有更好的方法怎么办呢?

绕过 GIL 的线索就在名字里,全局解释器锁是全局解释器状态的一部分。CPython 进程可以有多个解释器,因此可以有多个锁,但是此功能很少使用,它仅通过 C-API 公开。

CPython 3.8 提出的功能之一是 PEP 554,它是子解释器的实现以及标准库中带有新解释器模块的 API。

这使得能够在单个进程中创建多个Python解释器。

Python 3.8 的另一个变化是解释器都将拥有单独的 GIL —由于解释器状态包含内存分配区域,即指向 Python 对象(本地和全局)的所有指针的集合,因此 PEP 554 中的子解释器无法访问其他解释器的全局变量。

与多处理类似,在解释器之间共享对象的方法是序列化它们,并使用 IPC(网络、磁盘或共享内存)的形式。

Python 中序列化对象的方法有很多种,有 marshal 模块、pickle 模块以及更标准化的方法,如 json 和 simplexml。每一个都有优点和缺点,并且都有开销。

第一个奖励是拥有一个可变的共享内存空间,并由所属进程控制。这样,对象可以从主解释器发送并由其他解释器接收。这将是 PyObject 指针的查找托管内存空间,每个解释器都可以访问该空间,并由主进程控制锁。

看起来效率很低

marshal 模块的速度相当快,但还是不如直接从内存共享对象快。

PEP 574 提出了一种新的 pickle协议 (v5),该协议支持允许内存缓冲区与 pickle 流的其余部分分开处理。对于大型数据对象,一次性将它们全部序列化并从子解释器反序列化会增加大量开销。

好的,这个示例使用的是低级子解释器 API。如果您使用过多处理库,您就会认识到其中的一些问题。它不像线程那么简单,你不能只是说在单独的解释器中使用这个输入列表来运行这个函数(还)。

一旦这个 PEP 被合并,我希望我们会看到 PyPi 中的一些其他 API 采用它们。

子解释器有多少开销?

一处简短的回答:大于线程,小于进程。

一个长答案:解释器有自己的状态,因此虽然 PEP554 可以轻松创建子解释器,但它需要克隆和初始化以下内容:

  • __main__ 命名空间和 importlib 中的模块

  • sys 包含字典

  • 内置函数( print() 、断言等)

  • 线程

  • 核心配置

核心配置可以很容易地从内存中克隆,但导入的模块就没那么简单了。在 Python 中导入模块很慢,因此如果创建子解释器意味着每次都将模块导入到另一个命名空间中,那么优势就会减少。

异步怎么样?

标准库中 asyncio 事件循环可以实现要评估已创建的帧,但需要在主解释器内共享状态(共享 GIL)。

合并 PEP554 后,很可能在 Python 3.9 中,可以实现替代事件循环实现(尽管还没有人这样做),该实现在子解释器中运行异步方法,因此是并发的。

听起来不错,开始吧!嗯,但不完全是。

由于 CPython 长期以来一直使用单个解释器实现,代码库的许多部分都使用“运行时状态”而不是“解释器状态”,因此如果将 PEP554 合并到当前的形式,仍然会出现很多问题。

例如,垃圾收集器(<3.7)状态属于运行时。

在 PyCon 冲刺期间,更改已开始将垃圾收集器状态移至解释器,以便每个子解释器将拥有自己的 GC。

另一个问题是,CPython 代码库和许多 C 扩展中存在一些“全局”变量。因此,当人们开始编写“正确”的并发代码时,可能会看到另外一些问题。

包括文件句柄属于进程,如果你在一个解释器中打开一个文件进行写入,则子解释器将无法访问该文件。

总体来说,还有不少的其他事情需要解决。

结论:GIL到底消灭了吗?

对于单线程应用程序,GIL 还将仍然存在。因此说来,即使合并 PEP554,如果有单线程代码,它也不会执地并发特性。

如果您想在 Python 3.8 中使用并发代码,并且遇到 CPU 限制的并发问题,那么这可能就是问题所在。

Pickle v5 与用于多进程处理共享内存的实现是 Python 3.8(2019 年 10 月发布),子解释器是在 3.8 到 3.9 实现的。

如果现在想使用我的示例,这里已经构建了一个自定义分支,包含所需的所有代码。希望对各位有帮助:

https://github.com/tonybaloney/cpython/tree/subinterpreters

作者:安东尼 ·肖

参考:

https://hackernoon.com/has-the-python-gil-been-slain-9440d28fa93d

相关文章

塑造我成为 CTO 之路的“秘诀”
“人工智能教母”的公司估值达 10 亿美金
教授吐槽:985 高校成高级蓝翔!研究生基本废了,只为房子、票子……
Windows 蓝屏中断提醒开发者:Rust 比 C/C++ 更好
Claude 3.5 Sonnet 在伽利略幻觉指数中名列前茅
上海新增 11 款已完成登记生成式 AI 服务

发布评论