CPython的全局解释器锁-GIL即将成为历史

一直以来,Python 线程在执行 CPU 密集型操作时,都无法实现真正的并行性,因为线程之间会相互让出 CPU。Python 3.13 引入了无 GIL(全局解释器锁)的构建方式,这是自 CPython 解释器创建以来最大的架构性变化之一。Python 线程终于可以并行运行,以全速执行。

无 GIL 的 Python 直到最近还只是实验性功能,但随着 Python 3.14 beta 3 的发布,它已正式被支持。无 GIL 的构建方式仍然是可选的,可以选择尝鲜。

以下是一些入门建议:

  • 先问自己 Python 线程能为你做什么

Python 线程长期以来并不适合真正的并行性,因此直到现在,你可能从未考虑过它作为实现并行性的可能性。现在,随着无 GIL 的 Python 正式被支持,可以考虑它的使用场景了。

任何语言中的线程都采用"分而治之"的方法来处理任务。任何"明显适合并行"的任务都适合使用线程。但必须指出,并非所有问题都能在多个线程中均匀分割;有些问题在使用线程时可能会变得复杂,出现问题时,难以调试。

举个例子,如果你有一个任务需要写入大量文件,如果每个任务都单独运行在一个线程中,效率会较低,因为写入文件本身是一个串行 操作。更好的方法是将任务分配到多个线程中,并使用一个线程专门负责写入磁盘。当每个任务完成后,它将工作发送给磁盘写入任务。这样,任务之间不会互相阻塞,也不会因文件写入而被阻塞。

  • 使用最高层次的线程抽象

Python 中有多个层次的线程抽象:

你可以直接创建线程并使用 threading.Thread 来管理它们。这种情况下,每个线程的生命周期需要自己管理,并且还要管理线程完成后的等待和结果获取。这在程序中其他操作需要等待线程完成时是可行的,对于更高级的任务就不太理想。

concurrent.futures.ThreadPoolExecutor 是一个更高层次的抽象。它创建一个线程池,可以设置线程数量,以响应传入的请求。你可以将任务提交给线程池,并在需要时获取结果,这样程序就不会因等待任务完成而被阻塞。

这两种方法都是对底层 _thread 模块的抽象,而 _thread 模块本身则是对操作系统级线程处理的抽象。你使用的抽象层次越高,无 GIL 的 Python 行为越可能符合预期。

一般来说,任何在 Python 中使用的线程(而不是在外部,如扩展模块中使用的线程)都应该在 Python 中创建。理论上,你可以在 CPython 扩展中创建线程并将其注册到解释器中,但这样做并没有太大意义,因为解释器已经很好地完成了这些工作。

如果你已经在使用 ProcessPoolExecutor 作为抽象,那么在无 GIL 的构建中,你可以轻松地将其替换为 ThreadPoolExecutor。两者的接口相同,因此只需修改导入语句即可。

  • 确保 Cython 模块是线程安全的

无 GIL 的 Python 最大的障碍之一是确保用 C(或具有 C 兼容接口的语言)编写的 CPython 扩展能够尊重新的无 GIL 设计。

Cython 是 Python 中编写 C 扩展的关键工具,它紧密跟踪 CPython 运行时的变化。最近,Cython 的维护者添加了对无 GIL 构建的支持,但你仍然需要确保你的代码是线程安全的。具体来说,当与 Python 对象交互时,代码必须是线程安全的。

如果你已经确信你的代码是线程安全的,你可以在 Cython 模块中添加一个指令,并在无 GIL 构建中测试它:

python 复制代码
# cython: freethreading_compatible = True

这将标记该模块与无 GIL 构建兼容。如果你在无 GIL 构建中导入一个未标记的模块,解释器会自动重新启用 GIL 以确保安全。

为了增加现有 Cython 模块的线程安全性,你可以使用 Cython 添加的几个工具来简化工作:

关键代码段(Critical sections):这是一个上下文管理器,它接受一个 Python 对象,并在上下文块的持续时间内为其创建一个 CPython 关键代码段或本地锁。它也可以作为函数装饰器使用,通常用于类方法(锁应用于类实例)。关键代码段会自动防止死锁,但代价是不能保证锁在整个关键代码段中持续持有------如果其他对象需要锁,锁可能会被释放并重新获取。

**PyMutex 锁:**这些是更强大的锁,需要显式获取和释放。请注意,如果你在非无 GIL 构建(如为了向后兼容)中使用它们,那么在 PyMutex 锁期间重新获取 GIL 会带来死锁的风险。

  • 不要在不同线程之间共享迭代器或帧对象

一些对象不应该在不同线程之间共享,因为它们的内部状态不是线程安全的。两个常见的例子是迭代器和帧对象。

Python 中的迭代器对象基于某些内部状态生成一个对象流。例如,生成器是创建迭代器对象的常见方式。

如果你创建了一个迭代器,不要尝试在不同线程之间传递它。你可以共享迭代器生成的对象(只要它们是线程安全的),但不要在不同线程之间共享迭代器本身。例如,要创建一个迭代器,按顺序生成字符串中的字母,你可以这样做:

python 复制代码
data = "abcdefg"
d_iter = iter(data)
item = next(d_iter)
item2 = next(d_iter)
# ... 等等

在这个例子中,`d_iter` 是迭代器对象。你可以在线程之间共享 `data` 和 `item`(或 `item2` 等),但不能在线程之间共享 `d_iter` 本身,因为这可能会破坏其内部状态。

Python 的帧对象包含程序在执行过程中的某个特定点的状态信息。它们被 Python 的调试机制用于在出现错误条件时提供有关程序的详细信息。

帧对象也不是线程安全的。如果你在 Python 程序中通过 `sys.current_frames()` 访问帧对象,那么在无 GIL 的构建中你可能会遇到问题。然而,如果你使用 `inspect.currentframe()` 或 `sys._getframe()`,只要不在线程之间共享它们,它们会更安全。

相关推荐
sali-tec1 小时前
C# 基于halcon的视觉工作流-章29-边缘提取-亚像素
开发语言·图像处理·算法·计算机视觉·c#
屁股割了还要学2 小时前
【数据结构入门】堆
c语言·开发语言·数据结构·c++·考研·算法·链表
HuiSoul2003 小时前
Spring MVC
java·后端·spring mvc
Flobby5293 小时前
Go 语言中的结构体、切片与映射:构建高效数据模型的基石
开发语言·后端·golang
hj10433 小时前
redis开启局域网访问
数据库·redis·缓存
lsx2024064 小时前
Vue.js 响应接口:深度解析与实践指南
开发语言
froginwe114 小时前
Vue.js 样式绑定
开发语言
摇滚侠5 小时前
面试实战 问题二十四 Spring 框架中循环依赖问题的解决方法
java·后端·spring
源代码•宸5 小时前
MySQL 索引:索引为什么使用 B+树?(详解B树、B+树)
数据结构·数据库·经验分享·b树·mysql·b+树·b-树
睡觉的时候不会困5 小时前
MySQL 数据库表操作与查询实战案例
数据库·mysql